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
+133
View File
@@ -0,0 +1,133 @@
# Security
This documents VibeSonic's security-relevant design decisions and the
configuration knobs that affect them. See also
[`docs/external-auth.md`](external-auth.md) for the trusted-proxy
authentication model in detail.
## Authentication
- **Passwords** are hashed with Argon2id (`internal/auth/password.go`).
VibeSonic never stores or logs a plaintext password.
- **Subsonic legacy token/salt auth** (`t=`/`s=` params, used by older
Subsonic clients) requires comparing an MD5 digest against one derived
from the stored password — but MD5 hashes aren't reversible into
Argon2id, so VibeSonic separately keeps an AES-256-GCM-encrypted copy
of the plaintext password (`internal/auth/subsonic_secret.go`), keyed
by a server-held encryption key, solely to support this legacy scheme.
This is an inherent property of the Subsonic protocol, not a VibeSonic
design choice — every Subsonic-compatible server needs some way to
recover the plaintext to validate `t=md5(password+salt)`. The
comparison itself uses a constant-time compare
(`crypto/subtle.ConstantTimeCompare`) to avoid a timing side-channel.
- **API keys** are stored and looked up by hash, never in plaintext.
- **Sessions** are random opaque tokens in an HTTP-only, `Secure`
(configurable via `VIBESONIC_COOKIE_SECURE`, on by default) cookie.
- **External (trusted-proxy) authentication** is off by default and,
when enabled, trusts an identity-asserting header only from
configured source IPs verified against the real TCP peer address —
never against spoofable headers like `X-Forwarded-For`. Full detail in
[`docs/external-auth.md`](external-auth.md).
- `/rest/*` (the Subsonic/OpenSubsonic API) authenticates independently
of external auth, via token/salt, password, or `apiKey`, so standard
Subsonic clients work unmodified regardless of how the browser UI is
fronted.
## CSRF
State-changing browser routes are protected by a double-submit CSRF
token (`internal/auth/csrf.go`), compared in constant time. The
`/rest/*` API is exempt by design — it's a machine API authenticated per
request (token/password/API key), not cookie-session-based, so CSRF
doesn't apply to it the way it does to browser form posts.
## Path traversal
Every file served — streamed audio, downloads, cover art — is resolved
through `internal/streaming.ResolvePath`, which joins the stored
relative path onto its music folder root, resolves symlinks, and
verifies the result is still inside that root before anything is opened.
A request cannot escape its folder via `../` segments or a symlink
pointing outside the folder.
## Podcast fetching: SSRF protections
Podcast feed and episode-enclosure URLs are, by nature, externally
supplied (an admin adds a feed URL; that feed's own content — including
HTTP redirects — is not otherwise controlled by VibeSonic). Without
restriction, this would let a malicious or compromised feed turn
VibeSonic into a proxy onto its own internal network: pointing a feed
URL, or a redirect from one, at `127.0.0.1`, an RFC1918 LAN address, or
a cloud metadata endpoint (`169.254.169.254`).
`internal/netguard` closes this off for every connection the podcast
manager's HTTP client makes, **including connections made while
following a redirect** (so a feed can't pass an initial URL check and
then redirect somewhere unsafe), by validating the actual IP address
about to be dialed:
- Loopback, RFC1918/RFC4193 private ranges, link-local (including the
`169.254.169.254` cloud metadata address), carrier-grade NAT
(`100.64.0.0/10`), multicast, and unspecified addresses are all
rejected.
- The check runs at dial time against the specific resolved IP, not a
hostname re-resolved later — so a DNS record that changes between the
initial check and the connection ("DNS rebinding") can't bypass it.
- Redirects are capped at 5 hops and restricted to `http`/`https`.
- Episode downloads are capped at 4 GB so a misbehaving or malicious
feed can't exhaust disk space via an unbounded response.
For deployments that intentionally host podcast feeds on a private
network, set `VIBESONIC_PODCASTS_ALLOW_PRIVATE_NETWORKS=true` to disable
the IP restriction (the size cap and redirect limit still apply). Leave
this off otherwise.
## Cover art / image handling
Source image files are size-capped before decoding, and — separately —
their *decoded pixel dimensions* are checked via `image.DecodeConfig`
before the full image is decoded into memory, rejecting anything over
40 megapixels. This prevents a small, highly-compressed image file from
forcing an out-of-proportion memory allocation ("decompression bomb").
## Transcoding
`ffmpeg`/`ffprobe` are invoked with `exec.CommandContext` and an
explicit argument-vector slice — never through a shell — so there is no
shell-metacharacter injection surface. Output paths are passed after a
literal `--` argument so a generated filename can never be
misinterpreted as an additional flag. Transcode concurrency is bounded
by a worker pool, and a client disconnect cancels the underlying
`ffmpeg` process via the request context.
## HTTP server hardening
The server sets `ReadHeaderTimeout` to bound how long a client can take
to send request headers, mitigating slow-header ("Slowloris"-style)
connection exhaustion. `WriteTimeout` is deliberately left unset:
audio/video streaming and transcoded output are long-lived writes that a
fixed write deadline would break.
## Redirects
All same-site post-action redirects (login's `next=`, playlist actions,
favorites, admin actions) are validated through a shared
`safeRedirectPath` helper that only accepts a same-site relative path —
rejecting protocol-relative (`//evil.example`) or absolute
(`https://evil.example`) targets — before being used in an
`http.Redirect`, so a crafted link can't be used to bounce a logged-in
user's browser off-site.
## Logging
VibeSonic logs operational events (startup/shutdown, migrations,
scanner activity, authentication failures, admin actions, transcode/
podcast failures) via structured logging. It never logs passwords,
session tokens, API keys, Subsonic auth tokens/salts, or `Authorization`
headers.
## Reporting a vulnerability
If you find a security issue, please report it privately rather than
opening a public issue, so a fix can be prepared before the details are
public.