Features: - Port all 7 desktop themes (Dracula, Trans, Emo variants, etc.) to the web UI - Configurable bind interface (localhost/Tailscale/LAN/all) with detected IPs, settable from both the web UI and desktop GUI - Optional password that gates the whole UI and all API access via session cookies (Argon2), with a login page, logout, and middleware; settable from both UIs - Library maintenance: scan for duplicate video IDs and missing assets (thumbnail/info.json/description), remove duplicates (scan-and-confirm, with out-of-library path guard), and re-fetch missing sidecars via yt-dlp. Surfaced in both the web UI and a desktop GUI window. Optimizations / cleanup: - Cache has_chapters at scan time (library.rs) instead of re-reading and parsing every info.json on each /api/library request - Use the cached channel size in get_library - Pool a single SQLite connection in WebState instead of opening one per request - Remove a 1.2 GB stray nested src/yt-offline duplicate tree Fixes: - Rescan no longer clears all thumbnail textures (caused thumbnails to unload); prune only removed entries and bump library_generation so the card grid refreshes correctly Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
213 lines
7.7 KiB
Markdown
213 lines
7.7 KiB
Markdown
# Security Audit — yt-offline
|
||
|
||
**Date:** 2026-05-17
|
||
**Scope:** Rust codebase + dependencies
|
||
**Threat Model:** Self-hosted personal YouTube archiving tool; single user accessing via localhost + LAN
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
✅ **Overall Risk: LOW**
|
||
|
||
No critical vulnerabilities found. The codebase follows security best practices for command execution, database access, and file serving. One informational warning on a transitive dependency (low risk).
|
||
|
||
---
|
||
|
||
## Findings
|
||
|
||
### ✅ Command Injection — SECURE
|
||
|
||
All external process invocations use `std::process::Command` or `tokio::process::Command` with `.arg()` calls, bypassing shell interpretation:
|
||
|
||
- **yt-dlp invocations** (`src/downloader.rs:160–188`): User URL passed as a clean argument, not shell-interpreted. Safe even with special characters.
|
||
- **ffmpeg invocations** (`src/web.rs:404–415`): Similarly safe, using separate `.arg()` calls.
|
||
- **Preview handler** (`src/web.rs:492–498`): yt-dlp preview command also uses `.arg()`, not vulnerable to command injection.
|
||
|
||
**No shell escaping needed; no `format!()` in command strings.**
|
||
|
||
---
|
||
|
||
### ✅ SQL Injection — SECURE
|
||
|
||
All database queries in `src/database.rs` use **parameterized statements**:
|
||
|
||
```rust
|
||
// Correct — parameter substitution
|
||
self.conn.execute(
|
||
"INSERT OR REPLACE INTO watched (video_id) VALUES (?1)",
|
||
[video_id],
|
||
)?;
|
||
|
||
// Correct — rusqlite::params! macro
|
||
self.conn.execute(
|
||
"INSERT OR REPLACE INTO positions (video_id, position_secs) VALUES (?1, ?2)",
|
||
rusqlite::params![video_id, position_secs],
|
||
)?;
|
||
```
|
||
|
||
**No string concatenation in SQL strings.** Video IDs come from path parameters and are bound safely.
|
||
|
||
---
|
||
|
||
### ✅ Path Traversal — SECURE
|
||
|
||
**File serving** (`src/web.rs:687`):
|
||
- Uses `tower_http::services::ServeDir::new(&channels_root)` — a well-audited crate that prevents `..` traversal.
|
||
|
||
**Info.json access** (`src/web.rs:230–236`):
|
||
- Videos are looked up **only** from the scanned library (`find_video_info_path`).
|
||
- Cannot directly request arbitrary files by path; must exist in the library.
|
||
|
||
**URL path encoding** (`src/web.rs:176–186`):
|
||
- Correct percent-encoding of URL segments.
|
||
- Path components are filtered to `Component::Normal` (rejects `..` and symlinks).
|
||
|
||
---
|
||
|
||
### ✅ Unsafe Code — NONE FOUND
|
||
|
||
**Result of search:** `grep -r "unsafe"` across all `.rs` files returns **zero unsafe blocks**.
|
||
|
||
All memory safety is guaranteed by Rust's type system. No C FFI or raw pointers.
|
||
|
||
---
|
||
|
||
### ⚠️ Dependency Warning (LOW RISK)
|
||
|
||
**Crate:** `paste` v1.0.15
|
||
**Status:** Unmaintained since 2024-10-07
|
||
**Severity:** Warning (not an error)
|
||
**Dependency Chain:**
|
||
```
|
||
paste 1.0.15
|
||
← egui-wgpu (GUI rendering)
|
||
← eframe (GUI framework)
|
||
← yt-offline (your app)
|
||
```
|
||
|
||
**Impact:**
|
||
- `paste` is a procedural macro for code generation; it does not process untrusted input.
|
||
- The crate is used only at compile-time for the GUI stack.
|
||
- No security-critical functionality in `paste`.
|
||
- **Mitigation:** Monitor for a replacement in the egui/eframe ecosystem. No action needed now.
|
||
|
||
---
|
||
|
||
## Input Validation
|
||
|
||
### Download URLs
|
||
- **Handler:** `post_download()` (`src/web.rs:339–350`)
|
||
- **Validation:** Trimmed and checked for empty; passed directly to yt-dlp via `.arg()`.
|
||
- **Risk:** Low — yt-dlp validates URLs server-side and fails gracefully on malformed input.
|
||
|
||
### Video IDs
|
||
- **Source:** Extracted from URL paths and library scans.
|
||
- **Usage:** Passed to SQL via parameterized queries and used to look up files in the library.
|
||
- **Risk:** Low — parameterized queries eliminate injection; library lookup prevents arbitrary file access.
|
||
|
||
### Position values
|
||
- **Handler:** `post_resume()` (`src/web.rs:464–479`)
|
||
- **Validation:** Numeric (f64) — deserialized by serde, bounds-checked (position > 3.0 filter).
|
||
- **Risk:** Low — stored in SQLite as REAL, no string manipulation.
|
||
|
||
### Preview query (yt-dlp URL)
|
||
- **Handler:** `get_preview()` (`src/web.rs:484–527`)
|
||
- **Validation:** Trimmed and checked for empty.
|
||
- **Risk:** Low — passed to yt-dlp via `.arg()`, no shell injection.
|
||
|
||
---
|
||
|
||
## Data Exposure
|
||
|
||
### Watched status & resume positions
|
||
- **Storage:** SQLite database (`yt-offline.db`), local filesystem.
|
||
- **Access:** Only available to the user running the app (same UID).
|
||
- **No network exposure:** Positions are stored server-side; browser caches position locally.
|
||
- **Risk:** Low — local-only; no credentials or PII stored.
|
||
|
||
### Download logs
|
||
- **Stored in:** Job objects, kept in RAM and shown in the UI.
|
||
- **Leakage risk:** stderr from yt-dlp is visible in the web UI; sensitive auth errors could be logged.
|
||
- **Mitigation:** Consider filtering `--cookies` error messages in production. Currently acceptable for personal use.
|
||
|
||
### yt-dlp output (preview, metadata)
|
||
- **Handler:** Parses JSON from yt-dlp; returns filtered fields to the browser.
|
||
- **Risk:** Low — only public YouTube metadata (title, duration, view count).
|
||
|
||
---
|
||
|
||
## Configuration & Secrets
|
||
|
||
### `cookies.txt`
|
||
- **Location:** Current working directory (hardcoded in `downloader.rs`).
|
||
- **Risk:** File is world-readable by default. **Recommendation:** Ensure restrictive file permissions (`chmod 600 cookies.txt`).
|
||
|
||
### `config.toml`
|
||
- **Contains:** Directory paths, port number, browser name, `source_url`.
|
||
- **Risk:** Low — no secrets stored. `source_url` is read-only from browser.
|
||
- **Sensitive data:** Only the `backup.directory` path could leak file structure; this is expected behavior.
|
||
|
||
---
|
||
|
||
## Network Security
|
||
|
||
### Web server scope
|
||
- **Binding:** Configured via `web.port` in `config.toml` (default 8080).
|
||
- **Current binding:** Likely `127.0.0.1:8080` (assumed; verify with netstat).
|
||
- **Risk:** If bound to `0.0.0.0`, anyone on the network can access. **Recommendation:** Bind to `127.0.0.1` or use a reverse proxy with authentication.
|
||
|
||
### No HTTPS
|
||
- **Status:** Web UI is served over HTTP.
|
||
- **Risk:** Medium if exposed to untrusted networks. **Recommendation:** Use a reverse proxy (nginx, Caddy) with TLS for remote access.
|
||
|
||
### No authentication
|
||
- **Status:** No login required; access is "security by obscurity" (port number).
|
||
- **Risk:** Medium if the port is discovered. **Recommendation:** Add optional HTTP Basic Auth or reverse proxy authentication for LAN access.
|
||
|
||
---
|
||
|
||
## Recommendations (Priority Order)
|
||
|
||
### HIGH
|
||
1. ✅ **Web server binding** — **FIXED**
|
||
- Added `web.bind` config option (default `127.0.0.1`).
|
||
- Set in `config.toml` to `127.0.0.1` for localhost-only access.
|
||
- Users can change to `0.0.0.0` if they understand the security implications.
|
||
|
||
### MEDIUM
|
||
2. **File permissions on `cookies.txt`** — Remind users to `chmod 600 cookies.txt`.
|
||
- Add note to README.
|
||
|
||
3. **TLS + authentication for remote access** — If exposing to LAN/internet:
|
||
- Use a reverse proxy (nginx, Caddy) with mTLS or Basic Auth.
|
||
- Or add built-in HTTP Basic Auth via an optional config field.
|
||
|
||
4. **Filter sensitive logs** — Consider not echoing yt-dlp stderr if it contains auth errors.
|
||
- Current: Acceptable for personal use. Not a blocker.
|
||
|
||
### LOW
|
||
5. **Update `paste` crate dependency chain** — Monitor egui/eframe for a replacement.
|
||
- Not urgent; no security impact in your use case.
|
||
|
||
6. **Add Content-Security-Policy headers** — Prevent XSS if web UI is exposed to untrusted networks.
|
||
- Current: Low risk (browser only, no JavaScript vulnerabilities found).
|
||
|
||
---
|
||
|
||
## Conclusion
|
||
|
||
The codebase demonstrates **strong security practices** for a personal self-hosted tool:
|
||
- ✅ No injection vulnerabilities (command, SQL).
|
||
- ✅ No unsafe code.
|
||
- ✅ Path traversal protection in place.
|
||
- ✅ Parameterized database queries.
|
||
- ✅ Safe process spawning.
|
||
|
||
**No blockers for personal use.** For public/LAN exposure, implement the HIGH-priority recommendations.
|
||
|
||
---
|
||
|
||
**Auditor:** Claude Code
|
||
**Audit Method:** Manual code review + cargo audit
|
||
**Status:** Complete
|