Files
vibesonic/docs/security.md
T
2026-08-23 14:13:18 -05:00

7.7 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 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 cookie. Set VIBESONIC_COOKIE_SECURE=true for HTTPS deployments so browsers only send session and CSRF cookies over secure connections.
  • 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.
  • /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").

Artist photos and manual metadata editing

When VIBESONIC_ENABLE_ARTIST_PHOTOS is on, VibeSonic looks up an artist photo from fanart.tv (only for an artist with both a tagged MusicBrainz ID and VIBESONIC_FANART_TV_API_KEY set) or, failing that, by searching Wikidata directly by artist name and reading the matched entity's image from Wikimedia Commons — both externally-hosted services, same trust model as the existing MusicBrainz/Cover Art Archive lookups used elsewhere. This path deliberately never calls the MusicBrainz API itself (its public instance proved too rate-limited to depend on); a name-only match is necessarily less precise, so it requires an exact (case-insensitive) label match and only weakly prefers a music-flavored Wikidata description, rather than fabricating a stricter guarantee it can't actually make. The resulting image download, and any admin-supplied photo/cover-art URL entered on an artist or album's edit page, is fetched through the same internal/netguard SSRF-safe client described above (dial-time IP validation, capped redirects, size-limited response). Uploaded photo/cover files are validated by actually decoding them as an image before being written to disk.

Editing artist/album/song metadata (/admin/artists/{id}/edit, /admin/albums/{id}/edit, /admin/songs/{id}/edit) and overriding a photo/cover are admin-only actions, gated the same way as every other /admin/* route.

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.