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
6.2 KiB
Security
This documents VibeSonic's security-relevant design decisions and the
configuration knobs that affect them. See also
docs/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 validatet=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 viaVIBESONIC_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 indocs/external-auth.md. /rest/*(the Subsonic/OpenSubsonic API) authenticates independently of external auth, via token/salt, password, orapiKey, 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.254cloud 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.