mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Replace the filepath.Abs + string-prefix path-traversal check followed by os.Stat / os.ReadFile calls with an os.Root anchored at stockholmDir. CodeQL (go/path-injection, alerts 143–145) did not recognise the string-based validation as a sanitiser boundary; os.Root is the same OS-level barrier used in the sec3 datastore and recorder refactors. Changes: - Open os.OpenRoot(stockholmDir) in ServeStatic; all file ops go through root.Stat / root.Open instead of os.Stat / os.ReadFile. - Replace resolveStaticFile (returned absolute + relative paths) with resolveStaticRel (URL path → relative path only; no filesystem access, no traversal logic — the Root handles containment). - Directory → index.html fallback moved into ServeStatic via root.Stat. - Drop path/filepath import from static.go (no longer needed). - Update tests: resolveStaticFile unit tests become resolveStaticRel unit tests; directory and traversal cases become ServeStatic integration tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
5.9 KiB
Go
226 lines
5.9 KiB
Go
package stockholm
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// ---- injectBootstrap ----
|
|
|
|
func TestInjectBootstrap_InjectsBeforeHead(t *testing.T) {
|
|
html := []byte(`<html><head><title>T</title></head><body></body></html>`)
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{AppVersion: "27.0"}
|
|
|
|
got := string(injectBootstrap(html, state, cfg))
|
|
|
|
if !strings.Contains(got, "window.StockholmBrowserBootstrap") {
|
|
t.Error("expected bootstrap script to be injected")
|
|
}
|
|
|
|
scriptEnd := strings.Index(got, "</script>")
|
|
headEnd := strings.Index(got, "</head>")
|
|
|
|
if scriptEnd < 0 || headEnd < 0 || scriptEnd > headEnd {
|
|
t.Error("expected bootstrap script to appear before </head>")
|
|
}
|
|
}
|
|
|
|
func TestInjectBootstrap_Idempotent(t *testing.T) {
|
|
html := []byte(`<html><head><script>window.StockholmBrowserBootstrap = {};</script></head><body></body></html>`)
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
|
|
got := injectBootstrap(html, state, cfg)
|
|
|
|
if strings.Count(string(got), "StockholmBrowserBootstrap") != 1 {
|
|
t.Error("expected bootstrap not to be injected a second time")
|
|
}
|
|
}
|
|
|
|
func TestInjectBootstrap_NoHeadTag_ReturnsUnchanged(t *testing.T) {
|
|
html := []byte(`<html><body>no head here</body></html>`)
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
|
|
got := injectBootstrap(html, state, cfg)
|
|
|
|
if string(got) != string(html) {
|
|
t.Error("expected html to be returned unchanged when </head> is absent")
|
|
}
|
|
}
|
|
|
|
// ---- isBootstrapTarget ----
|
|
|
|
func TestIsBootstrapTarget(t *testing.T) {
|
|
cases := []struct {
|
|
path string
|
|
want bool
|
|
}{
|
|
{"index.html", true},
|
|
{"INDEX.HTML", true},
|
|
{"setup/index.html", true},
|
|
{"SETUP/INDEX.HTML", true},
|
|
{"js/app.js", false},
|
|
{"css/main.css", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
if got := isBootstrapTarget(tc.path); got != tc.want {
|
|
t.Errorf("isBootstrapTarget(%q) = %v, want %v", tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- resolveStaticRel ----
|
|
|
|
func TestResolveStaticRel_Normal(t *testing.T) {
|
|
rel := resolveStaticRel("/app.js")
|
|
if rel != "app.js" {
|
|
t.Errorf("expected rel = %q, got %q", "app.js", rel)
|
|
}
|
|
}
|
|
|
|
func TestResolveStaticRel_RootMapsToIndexHTML(t *testing.T) {
|
|
rel := resolveStaticRel("/")
|
|
if rel != "index.html" {
|
|
t.Errorf("expected rel = %q, got %q", "index.html", rel)
|
|
}
|
|
}
|
|
|
|
func TestResolveStaticRel_EmptyMapsToIndexHTML(t *testing.T) {
|
|
rel := resolveStaticRel("")
|
|
if rel != "index.html" {
|
|
t.Errorf("expected rel = %q, got %q", "index.html", rel)
|
|
}
|
|
}
|
|
|
|
// ---- ServeStatic path-traversal and directory tests ----
|
|
|
|
func TestServeStatic_DirectoryMapsToIndexHTML(t *testing.T) {
|
|
dir := t.TempDir()
|
|
subDir := filepath.Join(dir, "setup")
|
|
_ = os.MkdirAll(subDir, 0755)
|
|
_ = os.WriteFile(filepath.Join(subDir, "index.html"), []byte("<html><head></head><body>setup</body></html>"), 0644)
|
|
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
backendCfg := &BackendConfig{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/setup", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
ServeStatic(rec, req, dir, backendCfg, state, cfg)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestServeStatic_PathTraversalRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
backendCfg := &BackendConfig{}
|
|
|
|
// os.Root rejects any path that would escape the root directory.
|
|
req := httptest.NewRequest(http.MethodGet, "/../../../etc/passwd", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
ServeStatic(rec, req, dir, backendCfg, state, cfg)
|
|
|
|
if rec.Code == http.StatusOK {
|
|
t.Errorf("expected non-200 for path traversal attempt, got 200")
|
|
}
|
|
}
|
|
|
|
// ---- ServeStatic integration ----
|
|
|
|
func TestServeStatic_ServesHTMLWithBootstrap(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(dir, "index.html"),
|
|
[]byte(`<html><head></head><body></body></html>`), 0644)
|
|
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{AppVersion: "27.0"}
|
|
backendCfg := &BackendConfig{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
ServeStatic(rec, req, dir, backendCfg, state, cfg)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "StockholmBrowserBootstrap") {
|
|
t.Error("expected bootstrap to be injected in served HTML")
|
|
}
|
|
}
|
|
|
|
func TestServeStatic_NotFound(t *testing.T) {
|
|
dir := t.TempDir()
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
backendCfg := &BackendConfig{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/nonexistent.js", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
ServeStatic(rec, req, dir, backendCfg, state, cfg)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestServeStatic_HeadReturnsNoBody(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(dir, "index.html"),
|
|
[]byte(`<html><head></head><body></body></html>`), 0644)
|
|
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
backendCfg := &BackendConfig{}
|
|
|
|
req := httptest.NewRequest(http.MethodHead, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
ServeStatic(rec, req, dir, backendCfg, state, cfg)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
if rec.Body.Len() != 0 {
|
|
t.Errorf("expected empty body for HEAD, got %d bytes", rec.Body.Len())
|
|
}
|
|
}
|
|
|
|
func TestServeStatic_FrontendLoggingCookieSet(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(dir, "index.html"),
|
|
[]byte(`<html><head></head><body></body></html>`), 0644)
|
|
|
|
state := NewNativeState(t.TempDir())
|
|
cfg := &Config{}
|
|
backendCfg := &BackendConfig{FrontendLoggingLevel: 2}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
ServeStatic(rec, req, dir, backendCfg, state, cfg)
|
|
|
|
cookie := rec.Header().Get("Set-Cookie")
|
|
if !strings.Contains(cookie, "stockholmFrontendLoggingLevel=2") {
|
|
t.Errorf("expected logging cookie with level 2, got %q", cookie)
|
|
}
|
|
}
|