mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
fix(database): replace the stored session secret once on upgrade
The session cookie key is generated once and kept forever. Keys stored by older releases came from a weaker generator, and a stored value cannot be told apart from a current one, so drop it once through a seeder and let GetSecret store a fresh crypto/rand key on the next start. Admins log in again once after upgrading; fresh installs record the seeder up front.
This commit is contained in:
+18
-1
@@ -1527,7 +1527,7 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
}
|
||||
|
||||
if empty && isUsersEmpty {
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "DNSOutboundQTypeZeroFix", "WireguardDomainStrategyFix", "XdnsFinalmaskNamesAddrsFix", "UDPHopClientMaskFix", "HostVerifyPeerCertByNameBoolFix", "HostMihomoX25519Reset", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "NodeDownNotified", "ResetIpLimitNoFail2ban"}
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "SessionSecretRotation", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "DNSOutboundQTypeZeroFix", "WireguardDomainStrategyFix", "XdnsFinalmaskNamesAddrsFix", "UDPHopClientMaskFix", "HostVerifyPeerCertByNameBoolFix", "HostMihomoX25519Reset", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "NodeDownNotified", "ResetIpLimitNoFail2ban"}
|
||||
for _, name := range seeders {
|
||||
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
|
||||
return err
|
||||
@@ -1638,6 +1638,12 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "SessionSecretRotation") {
|
||||
if err := dropPreUpgradeSessionSecret(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "OutboundRemovedKeysFix") {
|
||||
if err := migrateOutboundRemovedKeys(); err != nil {
|
||||
return err
|
||||
@@ -1899,6 +1905,17 @@ func clearLegacyProxySettings() error {
|
||||
})
|
||||
}
|
||||
|
||||
// Secrets stored before v2.8.3 came from math/rand (a 31-bit clock seed up to v2.1.3);
|
||||
// GetSecret draws a crypto/rand one on the next start.
|
||||
func dropPreUpgradeSessionSecret() error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("key = ?", "secret").Delete(&model.Setting{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&model.HistoryOfSeeders{SeederName: "SessionSecretRotation"}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func migrateOutboundRemovedKeys() error {
|
||||
var setting model.Setting
|
||||
err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(&setting).Error
|
||||
|
||||
@@ -107,6 +107,56 @@ func TestEnsureFirstUserCreatesOneWorkingCredential(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Old releases drew the cookie-signing secret from a clock-seeded math/rand, and a
|
||||
// stored value cannot tell its generator, so an upgrade must replace it exactly once.
|
||||
func TestSessionSecretRotatesOnceOnUpgrade(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "x-ui.db")
|
||||
if err := InitDB(dbPath); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = CloseDB() })
|
||||
reopen := func() []string {
|
||||
t.Helper()
|
||||
if err := CloseDB(); err != nil {
|
||||
t.Fatalf("CloseDB: %v", err)
|
||||
}
|
||||
if err := InitDB(dbPath); err != nil {
|
||||
t.Fatalf("reopen InitDB: %v", err)
|
||||
}
|
||||
var secrets []string
|
||||
if err := db.Model(&model.Setting{}).Where("key = ?", "secret").Pluck("value", &secrets).Error; err != nil {
|
||||
t.Fatalf("read secret: %v", err)
|
||||
}
|
||||
return secrets
|
||||
}
|
||||
storeSecret := func(value string) {
|
||||
t.Helper()
|
||||
if err := db.Where("key = ?", "secret").Delete(&model.Setting{}).Error; err != nil {
|
||||
t.Fatalf("clear secret: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Setting{Key: "secret", Value: value}).Error; err != nil {
|
||||
t.Fatalf("store secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
storeSecret("kept-on-a-fresh-install")
|
||||
if got := reopen(); len(got) != 1 || got[0] != "kept-on-a-fresh-install" {
|
||||
t.Fatalf("fresh install secret = %q after restart, want it kept", got)
|
||||
}
|
||||
|
||||
storeSecret("rfBd56ti2SMtYvSgD5xAV0YU99zampta")
|
||||
if err := db.Where("seeder_name = ?", "SessionSecretRotation").Delete(&model.HistoryOfSeeders{}).Error; err != nil {
|
||||
t.Fatalf("clear seeder history: %v", err)
|
||||
}
|
||||
if got := reopen(); len(got) != 0 {
|
||||
t.Fatalf("secret = %q after upgrade, want the pre-upgrade secret dropped", got)
|
||||
}
|
||||
storeSecret("drawn-after-upgrade")
|
||||
if got := reopen(); len(got) != 1 || got[0] != "drawn-after-upgrade" {
|
||||
t.Fatalf("secret = %q, want the rotated secret kept so restarts do not log everyone out", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedClientsFromInboundJSON_IsIdempotentAgainstExistingClients(t *testing.T) {
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
|
||||
Reference in New Issue
Block a user