161 lines
7.7 KiB
Markdown
161 lines
7.7 KiB
Markdown
# 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.
|