Complete Phase 4: security hardening, player/playlist/admin UI, docs

Security:
- New internal/netguard package: podcast feed/enclosure fetching now
  refuses to dial non-public IPs (loopback/RFC1918/link-local/cloud
  metadata/CGNAT), validated per-connection including across redirects,
  closing an SSRF gap where a malicious or compromised feed URL could
  probe the server's internal network. Configurable via
  VIBESONIC_PODCASTS_ALLOW_PRIVATE_NETWORKS for intentional LAN feeds.
- Podcast episode downloads capped at 4GB (was unbounded, disk-exhaustion risk)
- Cover art decoding now rejects images above a 40-megapixel budget
  before full decode (decompression-bomb guard)
- HTTP server sets ReadHeaderTimeout (slowloris hardening)
- Admin folder-scan redirect now validated as same-site instead of
  trusting the raw Referer header (open-redirect fix)
- Legacy Subsonic token/salt auth now uses a constant-time comparison

Web UI completion:
- Persistent player: shuffle and repeat (off/all/one) controls, wired
  into queue advance/prev logic and persisted like other player state
- Admin user-permissions form: adminRole toggle (with a guard against
  demoting the last remaining admin) and per-user music-folder access
  checkboxes, backed by existing schema/enforcement that was previously
  unreachable from the UI
- Playlist M3U8 import/export

Docs: added docs/client-compatibility.md (honest tracking template —
no client testing has been performed), docs/deployment.md,
docs/security.md, docs/backup.md; refreshed README and cross-links;
added a footnote in docs/subsonic-compatibility.md distinguishing
spec-standard from VibeSonic-original OpenSubsonic extensions.

Also added Go fuzz tests for the external-auth CIDR/IP parser and the
LRC lyrics parser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
This commit is contained in:
2026-08-23 09:18:17 -05:00
co-authored by Claude Sonnet 5
parent 933ed31550
commit 789bf91eed
36 changed files with 1722 additions and 96 deletions
+111
View File
@@ -0,0 +1,111 @@
# Backup
VibeSonic keeps all of its state under one directory,
`VIBESONIC_DATA_DIR` (default `/var/lib/vibesonic`, or `/data` in the
provided Docker image). Everything that must be backed up lives there:
```
<DataDir>/
vibesonic.db SQLite database — users, library index,
playlists, favorites, sessions, API keys,
podcast subscriptions, shares, ...
vibesonic.db-wal SQLite write-ahead log (see below)
vibesonic.db-shm SQLite shared-memory index for the WAL
cache/covers/ resized cover-art cache (safe to delete —
regenerated on demand from source images)
podcasts/ downloaded podcast episodes
```
Your music library itself (the folders configured via
`VIBESONIC_MUSIC_DIRS` / the admin "Music Folders" page) is not inside
`DataDir` and has its own backup story — it's your source media, and
VibeSonic never modifies it.
## What must be backed up
1. **`vibesonic.db`** (plus `-wal`/`-shm` if present — see below). This
is the only thing whose loss is unrecoverable: it holds every user
account, password/API-key hash, the entire scanned library index,
playlists, favorites/ratings, play history, podcast subscriptions,
share links, and configuration stored in the `settings` table.
2. **`podcasts/`**, if you don't want to re-download subscribed
episodes after a restore.
3. Anything you passed via `VIBESONIC_*` environment variables
(systemd unit, Docker Compose file, `.env`) — these aren't stored in
the database and won't come back from a database restore alone.
## What does not need to be backed up
- **`cache/covers/`** — purely a derived cache, regenerated
automatically the first time each cover art size is requested again.
Safe to delete at any time, including while VibeSonic is running.
- **Your music files** — back these up through whatever process you
already use for your media library; VibeSonic treats them as
read-only input, never as its own state.
## Correct SQLite backup: mind the WAL
VibeSonic runs SQLite in **WAL (write-ahead log) mode** for concurrency
(`internal/database/database.go`: `PRAGMA journal_mode=WAL`). In WAL
mode, recently-committed data can live in the `vibesonic.db-wal` file
rather than in `vibesonic.db` itself, and isn't merged back
("checkpointed") into the main file on any fixed schedule.
**Do not just `cp vibesonic.db` from a live server.** A plain file copy
of `vibesonic.db` alone, taken while VibeSonic is running, can miss data
that only exists in the `-wal` file, or copy `vibesonic.db` and
`vibesonic.db-wal` at slightly different points in time and produce an
inconsistent pair. Either failure mode can leave you with a backup that
looks fine (opens without error) but is silently missing recent writes
or is outright corrupt.
Use one of these instead:
### Option A — stop the server, then copy (simplest, requires downtime)
```sh
systemctl stop vibesonic
cp /var/lib/vibesonic/vibesonic.db* /path/to/backup/
systemctl start vibesonic
```
On a clean shutdown SQLite checkpoints and closes the WAL cleanly, so a
plain copy of the (possibly WAL-less) database directory taken while the
process is stopped is always consistent.
### Option B — the SQLite online backup API, via the `sqlite3` CLI (no downtime)
`sqlite3`'s `.backup` command uses SQLite's own backup API, which
correctly handles an in-progress WAL and produces a consistent snapshot
without stopping VibeSonic or blocking writers for more than the brief
final copy step:
```sh
sqlite3 /var/lib/vibesonic/vibesonic.db ".backup /path/to/backup/vibesonic.db"
```
Do this on a schedule (cron/systemd timer) and rotate the output.
### Option C — filesystem snapshot (LVM, ZFS, Btrfs)
A crash-consistent filesystem snapshot of the whole `DataDir` (taken
atomically, e.g. `zfs snapshot` or an LVM snapshot) is also safe — SQLite
in WAL mode is designed to recover cleanly from a snapshot equivalent to
an ungraceful power loss, the same way it recovers on the next open
after a crash. Plain `rsync`/`cp` while the server is running is **not**
equivalent to this, since it isn't atomic across the `.db`/`-wal`/`-shm`
trio.
## Restoring
1. Stop VibeSonic.
2. Replace `vibesonic.db` (and `-wal`/`-shm` if you backed those up too)
with the backup copy.
3. Restore `podcasts/` if you backed it up.
4. Start VibeSonic. It runs pending migrations automatically on
startup, same as any other launch.
If you only have a `vibesonic.db` from Option B/C without matching
`-wal`/`-shm` files, that's fine — `.backup` and filesystem snapshots
both produce a database file that's already fully checkpointed/consistent
on its own.