diff --git a/docs/requirements.md b/docs/requirements.md index fc7cfff0..6dc31813 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -402,6 +402,7 @@ Internal architecture, components, and application logic. | DR-210 | Thumbnail cache writes are confined to the cache directory. The filename was built from `item_id`, `image_type` and `tag`, but only `tag` was sanitised — and `Path::join` neither folds `..` nor keeps the base when handed an absolute path, so a value arriving verbatim from server JSON decided where a file landed. The tag's existing rule (non-alphanumerics become `_`) now applies to all three parts, and the resolved path is checked with `starts_with(cache_dir)` at the point of use. The database keeps the raw key and the resolved path, so lookups still match and pre-existing rows still resolve. Not exploitable as shipped — server URLs must be HTTPS and Android blocks cleartext, so the id comes from a server the user chose to trust — the value is making the write path consistent with how caller-supplied paths are handled elsewhere | Storage | UR-012 | Done | | DR-211 | Download paths are confined to the download root. `file_path` and `target_dir` reached `PathBuf::join` unchecked from the frontend, and `mark_download_completed` persisted a caller-supplied path later passed to `remove_file`. A correct sanitiser already existed and `download_item_and_start` used it, but `download_item` is itself a command accepting `file_path` raw, so the guard was bypassable rather than absent — the fix moves it inside instead of adding a second one. Sanitising is **per path component**: whole-string sanitising would rewrite `downloads/x.mp3` to `downloads_x.mp3` and relocate every existing download. Confinement happens after the join, since a join with an absolute second half discards the root | Downloads | UR-011 | Done | | DR-212 | Query and URL construction bind or encode their inputs. Three sites interpolated caller-supplied values directly: the offline `get_items` item-type filter built `IN ('a','b')` by string formatting, `build_get_items_endpoint` wrote `ParentId`/`IncludeItemTypes`/`SortBy`/`SortOrder` into a URL unencoded, and `player_set_volume` accepted NaN and out-of-range floats. Each is a *consistency* defect rather than a novel one — the same file already did it correctly a few lines away (parameter placeholders in `search`, `urlencoding::encode` for genres, `clamp` in every player backend). List separators stay unencoded and encoding is per element, because Jellyfin splits these parameters on the comma | Repository | UR-007, UR-065 | Done | +| DR-213 | Containerised builds hand their artifacts back to the host user. The compose services bind-mount the repo and run as root — their caches live at `/root/.cargo` and `/root/.bun`, so a non-root container user cannot write them — which leaves root-owned files accumulating in the developer's working tree: 11,124 of them when this was found, enough that `cargo clean` and `scripts/clean.sh` failed with EACCES and a plain `cargo build` died part-way, since build scripts compile for the host and land in `target/debug` even during a cross-build. Ownership is restored at the end of each containerised build, reading the intended owner from the checkout so no uid needs plumbing through. Running the containers as the host uid is the tidier fix and remains open; it needs the cache volumes relocated off `/root` first | Tooling | - | Done | | DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer — through a future `{@html}`, a dependency, or a devtools paste — would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` — a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `