# Security Audit — yt-offline **Date:** 2026-05-23 (re-audit after security hardening commit `5999673`) **Earlier audit:** 2026-05-17 (commit pre-dating session auth, CSP, SHA-256 verify, etc.) **Scope:** Rust codebase + embedded web UI + dependencies **Threat model:** Self-hosted personal archiving tool. Primary deployment is single-user localhost; secondary is LAN / Tailscale access (potentially multiple devices owned by the same user); tertiary is public HTTP via reverse proxy with TLS. --- ## Summary ✅ **Overall risk: LOW** for localhost and Tailscale deployments. ⚠️ **MEDIUM** for plain-HTTP LAN exposure (mitigated by SameSite=Strict cookie + CSP; defense-in-depth, no transport encryption). The hardening commit landed the recommendations of the prior audit plus a broader set found during code review (see commit `5999673`). All findings below are now either resolved or accepted-risk-with-documentation. --- ## Threat model | Adversary | Capability | In scope? | |---|---|---| | Local user on same machine | Reads files owned by `luna` | Yes — DB chmod, cookies.txt | | LAN attacker on same subnet | Sees HTTP traffic, can connect to bound ports | Yes — bind policy, auth, CSP | | Remote attacker via Tailscale tailnet | Owned by same user, but still adversarial in principle | Yes | | Public internet attacker | Reaches server through misconfigured reverse proxy | Out of scope (not the intended deployment), but defenses still apply | | Browser-side XSS via injected video metadata | Compromised channel name / description from yt-dlp | Yes — primary XSS surface | | Supply-chain attack on bundled binaries | Malicious yt-dlp / deno served from GitHub | Yes — SHA-256 verify | The tool **does not** defend against an attacker with shell access to the machine, an attacker who can write to the channels directory, or compromise of upstream YouTube cookies the user has chosen to import. --- ## Findings ### ✅ Command injection — SECURE All external process invocations use `std::process::Command` / `tokio::process::Command` with separate `.arg()` calls; no shell interpretation. Sites audited: - yt-dlp downloads: `src/downloader.rs:start`, `start_music`, `repair` - yt-dlp preview: `src/web.rs:get_preview` (now goes through `apply_cookie_flags` and respects bundled-binary setting) - yt-dlp bundled install: `src/ytdlp_bin.rs:install_command` runs `bash -c` with a static script whose only interpolated values are paths under `~/.local/share/yt-offline/` and constant GitHub URLs — no user input flows in. - ffmpeg transcoder: `src/web.rs:get_transcode` (kill_on_drop set; child terminates when stream is dropped). Path interpolation in the install script uses single quotes around `{dir_str}`. The path is derived from `$HOME` via `std::env::var_os`. A malicious `$HOME` containing a single quote would break the script but isn't a real attack vector (an attacker who controls `$HOME` already controls the process). ### ✅ SQL injection — SECURE All queries in `src/database.rs` use parameterized statements (`?1` / `rusqlite::params!`). No string concatenation in SQL. Re-verified after the `settings` table was added. ### ✅ Path traversal — SECURE - `/files/*` and `/music-files/*`: served by `tower-http`'s `ServeDir`, which rejects `..` and refuses to follow symlinks out of the served root. - `/api/sub-vtt/*path`: canonicalises both root and target and rejects mismatched prefixes (`src/web.rs:get_sub_vtt`). - `/api/transcode/:id`, `/api/chapters/:id`, `/api/metadata/:id`, `/api/description/:id`: all resolve through `library::find_video`, which only returns files indexed by the scanner — direct path injection is impossible. - `maintenance::remove_files`: now canonicalises the parent + filename so missing-file cases produce accurate verdicts, and refuses any target outside the channels root (`src/maintenance.rs:is_within`). ### ✅ XSS in embedded web UI — DEFENDED - All user-influenced strings (channel names, video titles, descriptions, uploader names, IDs) are escaped via `esc()` before HTML interpolation. - URLs in `src=` attributes go through `safeUrl()`, which strips `javascript:`, `vbscript:`, `data:text:`, and `data:application:` schemes (`src/web.rs`). - `Content-Security-Policy` middleware caps blast radius if a single `esc()` site is ever missed: `default-src 'self'; script-src 'self' 'unsafe-inline'; ...; object-src 'none'; frame-ancestors 'none'`. - `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer` set globally. **Known limitation:** the embedded UI uses inline `