From 060a3e48db066d7b61056370f68b09728de0d9cf Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Fri, 15 May 2026 18:48:59 +0200 Subject: [PATCH] fix(security): CORS allowlist, URL scheme guard, state perms, ZIP slip, mirror docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Docs/plans/security-stream-token.md | 131 ++++++++++++++++++++++++++ internal/agent/mirror_client.go | 12 ++- internal/agent/state.go | 8 +- internal/cmd/daemon.go | 1 + internal/cmd/helpers.go | 17 +++- internal/cmd/helpers_test.go | 26 +++++ internal/config/config.go | 1 + internal/engine/stream_player.go | 28 +++++- internal/engine/stream_server.go | 90 ++++++++++++------ internal/engine/stream_server_test.go | 65 +++++++++++++ internal/engine/validate.go | 36 +++++++ internal/upgrade/extract.go | 58 ++++++++++-- internal/upgrade/upgrade_test.go | 37 ++++++++ 13 files changed, 462 insertions(+), 48 deletions(-) create mode 100644 Docs/plans/security-stream-token.md diff --git a/Docs/plans/security-stream-token.md b/Docs/plans/security-stream-token.md new file mode 100644 index 0000000..1a08e21 --- /dev/null +++ b/Docs/plans/security-stream-token.md @@ -0,0 +1,131 @@ +# Phase 2.2 — Per-task stream token (deferred) + +Status: deferred. Requires coordinated change in the web app +(`torrentclaw-web`) and the CLI daemon. Pulled out of the Phase 2 +security pass because the CLI-only fixes (UPnP opt-in, SSE caps, +signed self-update) ship without web-side work; the stream-token +work cannot. + +## Problem + +`/stream`, `/playlist.m3u` and `/hls//...` on the daemon +HTTP server have no authentication. Today, anyone who can reach the +listener and guesses (or learns) the `taskID` (for `/stream`) or +`sessionID` (for `/hls`) can fetch the active file. + +Mitigations already in place after Phase 1+2: + +- `sessionID` is restricted to a safe regex and is a server-issued + UUID v4 (122-bit entropy, not enumerable in practice). +- `/health` no longer leaks the active filename, taskID prefix or + client IP to remote callers (loopback diagnostics preserved). +- UPnP is opt-in, so by default the daemon is not exposed to the + public internet. +- The web client probes `/health` to pick LAN vs Tailscale. + +Residual risk: + +- On a shared LAN (open Wi-Fi, office network, dorm) any device can + reach the listener and brute-force `?id=` against + `/stream`. taskIDs are also UUIDs, so this is high entropy, but + the URL may leak through browser history, sharing, screen capture + or a passive logger and there is no second factor. +- A user who explicitly opts into UPnP exposes the same surface to + the entire internet. + +A per-task secret carried in the URL closes this without breaking +the `