fix(security): CORS allowlist, URL scheme guard, state perms, ZIP slip, mirror docs
Phase 3 security audit follow-up. Medium and low-severity hardenings plus a deferred-work plan for the cross-repo stream-token rollout. Stream server CORS: replace the wildcard Access-Control-Allow-Origin with an allowlist that echoes back only torrentclaw.com, app.torrentclaw.com, the local Next dev port (3030 — matches the web repo package.json) and any extras the operator adds via the new downloads.cors_extra_origins TOML key. A Vary: Origin header is now emitted whenever the request carries an Origin header so an intermediate cache cannot serve a stale ACAO to a different origin. URL scheme guard: openBrowser and OpenPlayer refuse any URL that is not http(s). Combined with passing the URL after "--" wherever the launched helper supports it (open, mpv, vlc, cvlc), this stops a leading "-" from being parsed as a switch by the spawned process. State file permissions: WriteState now writes 0o600 so the agent ID, PID and counters cannot be enumerated by another local user on a shared host. Matches the existing config file mode. ZIP slip defense-in-depth: extractZip extracts the safety check into safeZipPath, which canonicalises the entry name (normalising backslashes to "/"), rejects "..", "../" prefix and "/../" interior components, and verifies the final destination stays inside destDir before opening any file. Mirror fallback: documented the design for multi-provider mirrors.json hosting in the comment block on DefaultStaticFallbackURLs and added a follow-up note about signing it with the same ed25519 release key. The list is kept at one provider until the second host is provisioned and added to torrentclaw-web's STATIC_FALLBACKS. Deferred work: a new plan document Docs/plans/security-stream-token.md covers the per-task stream token (Phase 2.2 of the original audit) which requires coordinated web + CLI work and ships separately.
This commit is contained in:
parent
433e375def
commit
060a3e48db
13 changed files with 462 additions and 48 deletions
|
|
@ -429,6 +429,71 @@ func TestStreamServer_Health_NonLoopback_NoLeak(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestStreamServer_CORS_Allowlist verifica que sólo los origenes en la
|
||||
// allowlist reciben Access-Control-Allow-Origin y que ningún otro origen
|
||||
// es eco-reflejado.
|
||||
func TestStreamServer_CORS_Allowlist(t *testing.T) {
|
||||
srv := NewStreamServer(0)
|
||||
ctx := context.Background()
|
||||
if err := srv.Listen(ctx); err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
}
|
||||
defer srv.Shutdown(ctx)
|
||||
|
||||
cases := []struct {
|
||||
origin string
|
||||
wantAllow bool
|
||||
}{
|
||||
{"https://app.torrentclaw.com", true},
|
||||
{"https://torrentclaw.com", true},
|
||||
{"http://localhost:3030", true},
|
||||
{"http://127.0.0.1:3030", true},
|
||||
{"https://evil.example", false},
|
||||
{"null", false},
|
||||
{"", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.origin, func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodOptions, "/health", nil)
|
||||
if tc.origin != "" {
|
||||
req.Header.Set("Origin", tc.origin)
|
||||
}
|
||||
srv.healthHandler(rr, req)
|
||||
got := rr.Header().Get("Access-Control-Allow-Origin")
|
||||
if tc.wantAllow {
|
||||
if got != tc.origin {
|
||||
t.Errorf("origin %q: ACAO = %q, want %q", tc.origin, got, tc.origin)
|
||||
}
|
||||
} else if got != "" {
|
||||
t.Errorf("origin %q: ACAO leaked as %q, expected empty", tc.origin, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamServer_CORS_ExtraOrigin verifica que SetCORSAllowedOrigins añade
|
||||
// origins al baseline sin removerlos.
|
||||
func TestStreamServer_CORS_ExtraOrigin(t *testing.T) {
|
||||
srv := NewStreamServer(0)
|
||||
srv.SetCORSAllowedOrigins([]string{"https://custom.example"})
|
||||
ctx := context.Background()
|
||||
if err := srv.Listen(ctx); err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
}
|
||||
defer srv.Shutdown(ctx)
|
||||
|
||||
for _, origin := range []string{"https://custom.example", "https://torrentclaw.com"} {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
req.Header.Set("Origin", origin)
|
||||
srv.healthHandler(rr, req)
|
||||
if got := rr.Header().Get("Access-Control-Allow-Origin"); got != origin {
|
||||
t.Errorf("origin %q: ACAO = %q", origin, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamServer_HLS_InvalidSessionID verifica que el hlsHandler rechaza
|
||||
// session IDs con caracteres ilegales devolviendo 404 (uniforme con sesión
|
||||
// inexistente) para no filtrar el formato aceptado a un attacker.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue