# 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 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`](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.