diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go index 7c854a8e2..1afb8581e 100644 --- a/internal/server/routes_test.go +++ b/internal/server/routes_test.go @@ -159,13 +159,24 @@ func TestStaticRoutesWebOverlay(t *testing.T) { conf := config.NewMinimalTestConfig(t.TempDir()) webDir := conf.WebStoragePath() require.NoError(t, os.MkdirAll(filepath.Join(webDir, "node", "secrets"), fs.ModeDir)) + require.NoError(t, os.MkdirAll(filepath.Join(webDir, "config", "portal"), fs.ModeDir)) + require.NoError(t, os.MkdirAll(filepath.Join(webDir, "config", "certificates"), fs.ModeDir)) + require.NoError(t, os.MkdirAll(filepath.Join(webDir, "tls"), fs.ModeDir)) + require.NoError(t, os.MkdirAll(filepath.Join(webDir, "db"), fs.ModeDir)) require.NoError(t, os.MkdirAll(filepath.Join(webDir, "docs"), fs.ModeDir)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "options.yml"), []byte("blocked"), fs.ModeFile)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "Options.YML"), []byte("blocked"), fs.ModeFile)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "config.yaml"), []byte("blocked"), fs.ModeFile)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "id_rsa"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "auth.json"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "join_token"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "tls", "server.pem"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "db", "dump.sql"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "docs", "public.toml"), []byte("blocked"), fs.ModeFile)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "docs", "client_secret"), []byte("blocked"), fs.ModeFile)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "node", "secrets", "token.txt"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "config", "portal", "options.yml"), []byte("blocked"), fs.ModeFile)) + require.NoError(t, os.WriteFile(filepath.Join(webDir, "config", "certificates", "fullchain.pem"), []byte("blocked"), fs.ModeFile)) require.NoError(t, os.WriteFile(filepath.Join(webDir, "docs", "public.txt"), []byte("ok"), fs.ModeFile)) r := gin.New() @@ -177,8 +188,15 @@ func TestStaticRoutesWebOverlay(t *testing.T) { "/Options.YML", "/config.yaml", "/id_rsa", + "/auth.json", + "/join_token", + "/tls/server.pem", + "/db/dump.sql", + "/docs/public.toml", "/docs/client_secret", "/node/secrets/token.txt", + "/config/portal/options.yml", + "/config/certificates/fullchain.pem", } for _, filePath := range blocked { diff --git a/pkg/http/security/web_overlay.go b/pkg/http/security/web_overlay.go index 147b0557e..68b9972ce 100644 --- a/pkg/http/security/web_overlay.go +++ b/pkg/http/security/web_overlay.go @@ -11,21 +11,41 @@ import ( // overlayBlockedFileNames lists sensitive file base names that must never be // served by the web overlay, even when present under /storage/web. var overlayBlockedFileNames = map[string]struct{}{ - "client_secret": {}, - "config.yaml": {}, - "config.yml": {}, "id_ed25519": {}, "id_ed25519.pub": {}, "id_rsa": {}, "id_rsa.pub": {}, + "config.toml": {}, + "config.yaml": {}, + "config.yml": {}, + "defaults.yaml": {}, + "defaults.yml": {}, "options.yaml": {}, "options.yml": {}, + "hub.yml": {}, + "hub.yaml": {}, + "auth.json": {}, + "join_token": {}, + "client_secret": {}, +} + +// overlayBlockedFileExt lists sensitive file extensions that must never be +// served by the web overlay, even when present under /storage/web. +var overlayBlockedFileExt = map[string]struct{}{ + ".key": {}, + ".rsa": {}, + ".jwk": {}, + ".pem": {}, + ".sql": {}, + ".toml": {}, } // overlayBlockedPathPrefixes lists sensitive slash paths rooted under the // overlay tree that must always be denied (path-prefix match). var overlayBlockedPathPrefixes = []string{ "node/secrets", + "config/portal", + "config/certificates", } // OverlayHasAmbiguousPath returns true when a request path is unsafe due to @@ -109,11 +129,19 @@ func OverlayPathBlocked(webPath string) bool { } lowerPath := strings.ToLower(relPath) + lowerBase := path.Base(lowerPath) - if _, blocked := overlayBlockedFileNames[path.Base(lowerPath)]; blocked { + // Block requests for sensitive file names like "auth.json". + if _, blocked := overlayBlockedFileNames[lowerBase]; blocked { return true } + // Block file extensions commonly used for private keys or backups. + if _, blocked := overlayBlockedFileExt[path.Ext(lowerBase)]; blocked { + return true + } + + // Additionally check for sensitive path prefixes that should not be served. for _, prefix := range overlayBlockedPathPrefixes { if lowerPath == prefix || strings.HasPrefix(lowerPath, prefix+"/") { return true diff --git a/pkg/http/security/web_overlay_test.go b/pkg/http/security/web_overlay_test.go index 8bdb320a4..1fd461a68 100644 --- a/pkg/http/security/web_overlay_test.go +++ b/pkg/http/security/web_overlay_test.go @@ -75,7 +75,14 @@ func TestOverlayPathBlocked(t *testing.T) { {name: "SpecialDirBlocked", webPath: "__MACOSX/test.txt", blocked: true}, {name: "BlockedByName", webPath: "options.yml", blocked: true}, {name: "BlockedByNameCaseInsensitive", webPath: "Options.YML", blocked: true}, + {name: "BlockedByNameAuthJson", webPath: "auth.json", blocked: true}, + {name: "BlockedByNameJoinToken", webPath: "join_token", blocked: true}, + {name: "BlockedByExtensionPem", webPath: "tls/server.pem", blocked: true}, + {name: "BlockedByExtensionToml", webPath: "docs/public.toml", blocked: true}, + {name: "BlockedByExtensionSQL", webPath: "backup/database.sql", blocked: true}, {name: "BlockedByPrefix", webPath: "node/secrets/token.txt", blocked: true}, + {name: "BlockedByPrefixConfigPortal", webPath: "config/portal/options.yml", blocked: true}, + {name: "BlockedByPrefixCertificates", webPath: "config/certificates/fullchain.pem", blocked: true}, } for _, tc := range tests {