From 3c1e3cefc75e6763c57623ce0176b0185b01e3d5 Mon Sep 17 00:00:00 2001 From: smail Date: Thu, 1 Oct 2026 19:05:05 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E8=AF=81=E4=B9=A6=E9=A2=84=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=8A=A0=E5=9B=9E=E5=BD=92=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/migrate.go | 23 ++++++++++----- backend/config/migrate_test.go | 54 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/backend/config/migrate.go b/backend/config/migrate.go index dff53922..1a7cddce 100644 --- a/backend/config/migrate.go +++ b/backend/config/migrate.go @@ -115,6 +115,10 @@ var legacyPluginExecutableMap = map[string]string{ var legacyMySQLDSNPattern = regexp.MustCompile(`^([^:@/[:space:]]+):([^\r\n]*)@tcp\(([^():/[:space:]]+)(?::([0-9]{1,5}))?\)/([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:\?[^#[:space:]]*)?$`) func PlanLegacyMigration(root string, _ RuntimeValidationOptions) (LegacyMigrationPlan, error) { + return planLegacyMigration(root, false) +} + +func planLegacyMigration(root string, allowExternalSSL bool) (LegacyMigrationPlan, error) { directory, err := secureManagerConfigDirectory(root) if err != nil { return LegacyMigrationPlan{}, err @@ -144,7 +148,7 @@ func PlanLegacyMigration(root string, _ RuntimeValidationOptions) (LegacyMigrati if err := json.Unmarshal(data, &legacy); err != nil { return LegacyMigrationPlan{}, err } - plan.ManualFields = legacyManualFields(root, legacy) + plan.ManualFields = legacyManualFields(root, legacy, allowExternalSSL) if len(plan.ManualFields) > 0 { return plan, ErrMigrationUnsafeValue } @@ -155,7 +159,7 @@ func MigrateLegacyConfig(root string, options RuntimeValidationOptions) (LegacyM } func migrateLegacyConfig(root string, options RuntimeValidationOptions, completeSetup bool) (LegacyMigrationResult, error) { - plan, err := PlanLegacyMigration(root, options) + plan, err := planLegacyMigration(root, completeSetup) if err != nil { return LegacyMigrationResult{}, err } @@ -383,20 +387,25 @@ func equivalentStoredSecret(expected, existing StoredSecret, vault *credential.V return expectedErr == nil && existingErr == nil && expectedPlaintext == existingPlaintext } -func legacyManualFields(root string, legacy legacyMigrationConfig) []string { +func legacyManualFields(root string, legacy legacyMigrationConfig, allowExternalSSL ...bool) []string { + permitExternal := len(allowExternalSSL) > 0 && allowExternalSSL[0] + validReference := func(value string) bool { + _, ok := legacyControlledReference(value) + return ok || (permitExternal && filepath.IsAbs(strings.TrimSpace(value))) + } fields := make([]string, 0) _, _, databaseManualFields := legacyDatabasePlan(root, legacy) fields = append(fields, databaseManualFields...) - if _, ok := legacyControlledReference(legacy.SSLPublicKeyPath); legacy.SSLPublicKeyPath != "" && !ok { + if legacy.SSLPublicKeyPath != "" && !validReference(legacy.SSLPublicKeyPath) { fields = append(fields, "SSLPublicKeyPath") } - if _, ok := legacyControlledReference(legacy.SSLPrivateKeyPath); legacy.SSLPrivateKeyPath != "" && !ok { + if legacy.SSLPrivateKeyPath != "" && !validReference(legacy.SSLPrivateKeyPath) { fields = append(fields, "SSLPrivateKeyPath") } - if _, ok := legacyControlledReference(legacy.SSL.CertificateRef); legacy.SSL.CertificateRef != "" && !ok { + if legacy.SSL.CertificateRef != "" && !validReference(legacy.SSL.CertificateRef) { fields = append(fields, "ssl.certificateRef") } - if _, ok := legacyControlledReference(legacy.SSL.PrivateKeyRef); legacy.SSL.PrivateKeyRef != "" && !ok { + if legacy.SSL.PrivateKeyRef != "" && !validReference(legacy.SSL.PrivateKeyRef) { fields = append(fields, "ssl.privateKeyRef") } if legacy.SMTPUsername == "" && legacy.SMTPPassword != "" { diff --git a/backend/config/migrate_test.go b/backend/config/migrate_test.go index ec14e8c9..9f9edb7d 100644 --- a/backend/config/migrate_test.go +++ b/backend/config/migrate_test.go @@ -738,6 +738,60 @@ func TestMigrateLegacyConfigDerivesSetupStateFromFullPreflight(t *testing.T) { } } +func TestSetupMigrationAcceptsExternalSSLOnlyDuringFinalization(t *testing.T) { + for _, completing := range []bool{false, true} { + root := t.TempDir() + external := t.TempDir() + certificate := filepath.ToSlash(filepath.Join(external, "public.crt")) + privateKey := filepath.ToSlash(filepath.Join(external, "private.key")) + legacy := map[string]any{ + "domain": "example.com", "webDomain": "web.example.com", + "dbDSN": "./data/smail.db", "dbType": "sqlite", + "isInit": false, "setupState": "bootstrap", + "ssl": map[string]any{ + "management": SSLManagementWebServer, + "mode": ManagedSSLModeUpload, "defaultDirectory": "ssl", + "customDirectory": "ssl", "certificateRef": certificate, + "privateKeyRef": privateKey, + }, + } + writeLegacyMigrationFixture(t, root, legacy) + options := RuntimeValidationOptions{HasSuperAdmin: true, Production: true} + if _, err := PlanLegacyMigration(root, options); !errors.Is(err, ErrMigrationUnsafeValue) { + t.Fatalf("ordinary plan must reject external references: %v", err) + } + _, err := migrateLegacyConfig(root, options, completing) + if !completing { + if !errors.Is(err, ErrMigrationUnsafeValue) { + t.Fatalf("ordinary migration must reject external references: %v", err) + } + continue + } + if err != nil { + t.Fatal(err) + } + runtime, err := NewConfigLoader(root, ConfigLoaderOptions{Validation: options}).Load() + if err != nil { + t.Fatal(err) + } + if runtime.SetupState != SetupStateReady || runtime.SSL.TLS.CertificateRef != certificate || runtime.SSL.TLS.PrivateKeyRef != privateKey { + t.Fatalf("setup did not preserve external SSL references: %+v", runtime.SSL.TLS) + } + } +} + +func TestSetupMigrationStillRejectsUnsafeRelativeSSL(t *testing.T) { + legacy := legacyMigrationConfig{DbType: "sqlite", DbDSN: "./data/smail.db"} + legacy.SSL.CertificateRef = "../outside/public.crt" + legacy.SSL.PrivateKeyRef = "../outside/private.key" + if fields := legacyManualFields(t.TempDir(), legacy, true); len(fields) == 0 { + t.Fatal("setup preflight accepted unsafe relative SSL paths") + } + if _, fields := legacySSLConfig(legacy, true); len(fields) == 0 { + t.Fatal("setup conversion accepted unsafe relative SSL paths") + } +} + func TestMigrateLegacyConfigAcceptsStructuredSSLObject(t *testing.T) { root := t.TempDir() legacy := map[string]any{