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
+111
View File
@@ -0,0 +1,111 @@
# Backup
VibeSonic keeps all of its state under one directory,
`VIBESONIC_DATA_DIR` (default `/var/lib/vibesonic`, or `/data` in the
provided Docker image). Everything that must be backed up lives there:
```
<DataDir>/
vibesonic.db SQLite database — users, library index,
playlists, favorites, sessions, API keys,
podcast subscriptions, shares, ...
vibesonic.db-wal SQLite write-ahead log (see below)
vibesonic.db-shm SQLite shared-memory index for the WAL
cache/covers/ resized cover-art cache (safe to delete —
regenerated on demand from source images)
podcasts/ downloaded podcast episodes
```
Your music library itself (the folders configured via
`VIBESONIC_MUSIC_DIRS` / the admin "Music Folders" page) is not inside
`DataDir` and has its own backup story — it's your source media, and
VibeSonic never modifies it.
## What must be backed up
1. **`vibesonic.db`** (plus `-wal`/`-shm` if present — see below). This
is the only thing whose loss is unrecoverable: it holds every user
account, password/API-key hash, the entire scanned library index,
playlists, favorites/ratings, play history, podcast subscriptions,
share links, and configuration stored in the `settings` table.
2. **`podcasts/`**, if you don't want to re-download subscribed
episodes after a restore.
3. Anything you passed via `VIBESONIC_*` environment variables
(systemd unit, Docker Compose file, `.env`) — these aren't stored in
the database and won't come back from a database restore alone.
## What does not need to be backed up
- **`cache/covers/`** — purely a derived cache, regenerated
automatically the first time each cover art size is requested again.
Safe to delete at any time, including while VibeSonic is running.
- **Your music files** — back these up through whatever process you
already use for your media library; VibeSonic treats them as
read-only input, never as its own state.
## Correct SQLite backup: mind the WAL
VibeSonic runs SQLite in **WAL (write-ahead log) mode** for concurrency
(`internal/database/database.go`: `PRAGMA journal_mode=WAL`). In WAL
mode, recently-committed data can live in the `vibesonic.db-wal` file
rather than in `vibesonic.db` itself, and isn't merged back
("checkpointed") into the main file on any fixed schedule.
**Do not just `cp vibesonic.db` from a live server.** A plain file copy
of `vibesonic.db` alone, taken while VibeSonic is running, can miss data
that only exists in the `-wal` file, or copy `vibesonic.db` and
`vibesonic.db-wal` at slightly different points in time and produce an
inconsistent pair. Either failure mode can leave you with a backup that
looks fine (opens without error) but is silently missing recent writes
or is outright corrupt.
Use one of these instead:
### Option A — stop the server, then copy (simplest, requires downtime)
```sh
systemctl stop vibesonic
cp /var/lib/vibesonic/vibesonic.db* /path/to/backup/
systemctl start vibesonic
```
On a clean shutdown SQLite checkpoints and closes the WAL cleanly, so a
plain copy of the (possibly WAL-less) database directory taken while the
process is stopped is always consistent.
### Option B — the SQLite online backup API, via the `sqlite3` CLI (no downtime)
`sqlite3`'s `.backup` command uses SQLite's own backup API, which
correctly handles an in-progress WAL and produces a consistent snapshot
without stopping VibeSonic or blocking writers for more than the brief
final copy step:
```sh
sqlite3 /var/lib/vibesonic/vibesonic.db ".backup /path/to/backup/vibesonic.db"
```
Do this on a schedule (cron/systemd timer) and rotate the output.
### Option C — filesystem snapshot (LVM, ZFS, Btrfs)
A crash-consistent filesystem snapshot of the whole `DataDir` (taken
atomically, e.g. `zfs snapshot` or an LVM snapshot) is also safe — SQLite
in WAL mode is designed to recover cleanly from a snapshot equivalent to
an ungraceful power loss, the same way it recovers on the next open
after a crash. Plain `rsync`/`cp` while the server is running is **not**
equivalent to this, since it isn't atomic across the `.db`/`-wal`/`-shm`
trio.
## Restoring
1. Stop VibeSonic.
2. Replace `vibesonic.db` (and `-wal`/`-shm` if you backed those up too)
with the backup copy.
3. Restore `podcasts/` if you backed it up.
4. Start VibeSonic. It runs pending migrations automatically on
startup, same as any other launch.
If you only have a `vibesonic.db` from Option B/C without matching
`-wal`/`-shm` files, that's fine — `.backup` and filesystem snapshots
both produce a database file that's already fully checkpointed/consistent
on its own.
+69
View File
@@ -0,0 +1,69 @@
# Client compatibility
This is a **tracking template**, not a report of testing that has
already happened. As of this phase, no real Subsonic/OpenSubsonic
client apps have been run against VibeSonic in this development
environment — every row below is unfilled and marked accordingly. Please
do not read the table as evidence of compatibility; read
[`docs/subsonic-compatibility.md`](subsonic-compatibility.md) for that —
it documents endpoint-by-endpoint protocol conformance validated by
VibeSonic's own automated HTTP-level test suite (`internal/subsonic/*_test.go`),
which is real evidence, exercised on every change. This document exists
to capture *real device/app* testing once someone actually does it.
## How to test a client and fill in a row
1. Point the client at your VibeSonic server URL, e.g.
`https://music.example.com` or `http://<host>:8080` for a direct,
un-proxied connection. Most Subsonic clients want the base URL only
(no `/rest` suffix) plus a username/password — they construct
`/rest/*` requests themselves.
2. **Authentication**: try native username/password first (works whether
or not external auth is enabled, as long as the account has a local
password — see [`external-auth.md`](external-auth.md)). If the client
supports OpenSubsonic API keys (the `apiKeyAuthentication` extension —
see [`subsonic-compatibility.md`](subsonic-compatibility.md#opensubsonic-extensions)),
generate one from the web UI's account settings and test that path
too, noting which the client actually used.
3. Work through: server discovery/ping, browsing (artists/albums/songs,
both filesystem and ID3 modes if the client exposes both), search,
cover art, direct streaming, forced transcoding (pick a bitrate/format
the source file doesn't already match), playlists (create/edit/
delete/reorder), favorites/star, scrobbling, lyrics (plain and
synced, if the client supports OpenSubsonic's `songLyrics`), the play
queue, downloads, podcasts (if the client has podcast support), and
any OpenSubsonic-specific features the client advertises.
4. Fill in the row below with what actually worked, partially worked, or
didn't — and describe **Known Issues** concretely (not "broken", but
"album art doesn't load for tracks with only embedded art, HTTP 404"
or similar) so someone else can reproduce or fix it.
5. If a client needs a compatibility workaround (bending strict protocol
behavior to accommodate something the client gets wrong), that
exception must be documented here **and** in
[`subsonic-compatibility.md`](subsonic-compatibility.md) next to the
affected endpoint — don't silently special-case a client in code
without a paper trail.
## Tracking table
| Client | Version | Platform | Authentication | Browsing | Streaming | Transcoding | Playlists | Lyrics | OpenSubsonic | Known Issues |
|---|---|---|---|---|---|---|---|---|---|---|
| Symfonium | — | Android | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| Ultrasonic | — | Android | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| Tempo | — | iOS | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| Amperfy | — | iOS/macOS | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| play:Sub | — | iOS | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| Subtracks | — | Android | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| DSub | — | Android | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| Supersonic | — | Desktop (Linux/macOS/Windows) | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
| Feishin | — | Desktop/Web | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | Not yet tested | — |
Add new rows for any other client you test — this list isn't exhaustive,
just a reasonable starting set across ecosystems (Android, iOS, desktop).
## Compatibility exceptions
None recorded yet. If a real client requires bending protocol behavior
to work correctly, document the exception here: which client/version,
what VibeSonic does differently and why, and a link to the affected
endpoint in [`subsonic-compatibility.md`](subsonic-compatibility.md).
+2
View File
@@ -17,6 +17,8 @@ except the session secret.
| `VIBESONIC_SCAN_INTERVAL` | `1h` | How often to rescan enabled music folders, as a Go duration (`30m`, `6h`, ...). A startup scan always runs regardless of this value. |
| `VIBESONIC_SESSION_SECRET` | *(none)* | Reserved for future use hardening session cookies beyond their database-backed random IDs. Not required to run, but recommended to set (e.g. `openssl rand -hex 32`) — VibeSonic logs a warning at startup if it's empty. |
| `VIBESONIC_COOKIE_SECURE` | `true` | Whether session/CSRF cookies get the `Secure` attribute. Set to `false` only for local development over plain HTTP. |
| `VIBESONIC_ENABLE_EXTERNAL_METADATA` | `false` | Enable optional MusicBrainz/Cover Art Archive lookups for songs/albums that already carry a locally-tagged MusicBrainz ID. VibeSonic works fully offline with this off; it's the only feature that makes outbound Internet requests. |
| `VIBESONIC_PODCASTS_ALLOW_PRIVATE_NETWORKS` | `false` | Allow podcast feed/enclosure URLs to resolve to loopback/LAN/link-local addresses. Leave this off unless you intentionally host podcast feeds on a private network — see [`docs/security.md`](security.md#podcast-fetching-ssrf-protections). |
## External authentication
+230
View File
@@ -0,0 +1,230 @@
# Deployment
VibeSonic is a single static binary plus `ffmpeg`/`ffprobe` on `PATH`
(and optionally `mpv`, for the jukebox feature). Docker is convenient
but never required for normal operation.
## Configuration
All configuration is via `VIBESONIC_*` environment variables — see
[`docs/configuration.md`](configuration.md) for the full list. There is
no config file.
## Running natively
```sh
go build -o vibesonic ./cmd/vibesonic
export VIBESONIC_DATA_DIR=/var/lib/vibesonic
export VIBESONIC_MUSIC_DIRS=/music
export VIBESONIC_SESSION_SECRET="$(openssl rand -hex 32)"
./vibesonic
```
For a persistent deployment, see
[`systemd/vibesonic.service`](../systemd/vibesonic.service) — install it
with:
```sh
sudo useradd --system --home /var/lib/vibesonic --shell /usr/sbin/nologin vibesonic
sudo install -m 0755 vibesonic /usr/local/bin/vibesonic
sudo cp systemd/vibesonic.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now vibesonic
```
The unit runs as a dedicated `vibesonic` user (not `DynamicUser`,
because it needs a stable UID with read access to your music library —
`DynamicUser` allocates a random UID per start, which doesn't compose
well with a fixed set of file permissions on external media). Grant that
user read access to your music folders:
```sh
sudo usermod -aG <group-that-owns-your-music> vibesonic
# or, more simply, make the music tree group- or world-readable
```
`StateDirectory=vibesonic` in the unit creates and owns
`/var/lib/vibesonic` (mode `0700`) for you — don't create it manually
with different permissions first. The unit also applies standard
hardening (`ProtectSystem=strict`, `NoNewPrivileges`, `PrivateDevices`,
etc.) and `Restart=on-failure`.
Remember to set a real `VIBESONIC_SESSION_SECRET` in the unit (or in an
`EnvironmentFile=`) — the shipped example is a placeholder.
## Running with Docker
```sh
docker build -t vibesonic .
```
Runtime image is `debian:bookworm-slim` with `ffmpeg`, `mpv`, and CA
certificates installed, running as a non-root `vibesonic` user (uid
10001). See [`Dockerfile`](../Dockerfile).
Example Compose file (also in [`docker-compose.yml`](../docker-compose.yml)):
```yaml
services:
vibesonic:
image: vibesonic
ports:
- "8080:8080"
volumes:
- ./data:/var/lib/vibesonic
- /mnt/music:/music:ro
environment:
VIBESONIC_SESSION_SECRET: "change-me-to-a-random-string"
```
Mount your music read-only (`:ro`) — VibeSonic never writes to it.
`./data` (or a named volume) holds the SQLite database, cover art cache,
and downloaded podcast episodes; back it up per
[`docs/backup.md`](backup.md).
## Reverse proxy
Run VibeSonic behind a reverse proxy for TLS termination in any
real deployment; the examples below forward to VibeSonic on
`127.0.0.1:8080`.
VibeSonic does not use WebSockets. Streaming and transcoded responses
are ordinary long-lived HTTP responses using standard range requests —
make sure your proxy doesn't buffer the entire response body before
forwarding it (buffering breaks range requests and adds needless
latency to playback start).
### Caddy
```
music.example.com {
reverse_proxy 127.0.0.1:8080
}
```
Caddy handles TLS automatically and streams request/response bodies
without buffering by default — no extra configuration needed for range
requests or large responses.
### nginx
```nginx
server {
listen 443 ssl;
server_name music.example.com;
ssl_certificate /etc/letsencrypt/live/music.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/music.example.com/privkey.pem;
client_max_body_size 0;
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
}
}
```
`proxy_buffering off` matters here: nginx buffers proxied responses to
disk/memory by default, which delays the start of playback and range
responses. `client_max_body_size 0` avoids nginx rejecting large file
uploads if the upload feature is used.
### Traefik
```yaml
http:
routers:
vibesonic:
rule: "Host(`music.example.com`)"
service: vibesonic
tls:
certResolver: letsencrypt
services:
vibesonic:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
```
Traefik streams responses without buffering by default, same as Caddy.
## Reverse proxy + external (trusted-proxy) authentication
If you're using [`docs/external-auth.md`](external-auth.md) to have the
proxy handle SSO (OIDC, SAML, HTTP Basic Auth, ...) for the browser UI,
two things are essential:
1. **The proxy must strip any client-supplied identity header before
inserting its own.** If it doesn't, a client can set
`Remote-User: admin` (or whatever header name you configured) on its
own request and have the proxy forward it unchanged, impersonating
any user. Every proxy example below includes this.
2. **`/rest/*` must bypass the proxy's SSO layer.** Real Subsonic/
OpenSubsonic clients (mobile apps, desktop players) authenticate
directly against VibeSonic's own token/API-key scheme and have no way
to complete a browser-based SSO flow (OIDC redirect, SAML POST,
etc.). Exclude `/rest/*` from the SSO requirement so those clients
keep working, while still letting `/rest/*` reach VibeSonic normally
— VibeSonic's own `/rest/*` auth is independent of external auth
either way.
### Caddy example (with `forward_auth` for SSO + header stripping)
```
music.example.com {
# Strip any client-supplied identity header before forward_auth
# potentially sets a real one, so a client can't forge it.
header_down -Remote-User
@needsAuth {
not path /rest/*
}
forward_auth @needsAuth auth.example.com:9091 {
uri /verify
copy_headers Remote-User
}
reverse_proxy 127.0.0.1:8080
}
```
### nginx example (with `auth_request` for SSO + header stripping)
```nginx
server {
# ... TLS config as above ...
location /rest/ {
# Subsonic/OpenSubsonic clients: no SSO, straight through.
proxy_pass http://127.0.0.1:8080;
proxy_set_header Remote-User ""; # never forward a client-supplied value
}
location / {
auth_request /sso-verify;
auth_request_set $remote_user $upstream_http_remote_user;
proxy_set_header Remote-User $remote_user;
proxy_pass http://127.0.0.1:8080;
}
location = /sso-verify {
internal;
proxy_pass http://auth.example.com/verify;
proxy_set_header X-Original-URI $request_uri;
}
}
```
Either way, also set `VIBESONIC_EXTAUTH_TRUSTED_SOURCES` to only the
proxy's own address (e.g. `127.0.0.1/32` if it's on the same host) —
VibeSonic independently verifies the real TCP peer, so even a
misconfigured proxy can't be used to spoof identity from the internet
directly. See [`docs/external-auth.md`](external-auth.md) for that
check in detail.
+7 -3
View File
@@ -59,6 +59,10 @@ password on VibeSonic's own `/login` page even when external auth is
enabled, unless their account has no password hash set (auto-created
external users start this way).
In later phases, `/rest/*` (the native Subsonic API) will authenticate
exclusively via Subsonic's own token/API-key scheme and must never be
affected by external auth's browser-cookie flow.
`/rest/*` (the Subsonic/OpenSubsonic API) never consults external auth
at all: it authenticates exclusively via Subsonic's own token/salt,
password, or `apiKey` scheme (see `internal/subsonic/auth.go`), so real
Subsonic/OpenSubsonic clients keep working unmodified even when external
auth fronts the browser UI. This is also why a reverse proxy enforcing
SSO in front of VibeSonic should let `/rest/*` bypass that SSO layer —
see `docs/deployment.md` for example proxy configuration.
+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.
+10
View File
@@ -239,6 +239,16 @@ in `internal/subsonic/opensubsonic_core_test.go` fails the build if an
extension is ever registered without its handler actually existing, so
this list can't drift ahead of what's real.
Of the extensions below, `apiKeyAuthentication`, `formPost`, and
`songLyrics` come from the published OpenSubsonic extension list.
`getPodcastEpisode`, `indexBasedQueue`, `playbackReport`,
`topSongsByArtistId`, `transcodeOffset`, `transcoding`, and
`sonicSimilarity` are **VibeSonic-original** extensions — real,
implemented, and advertised the same way as any other extension, but
not part of the OpenSubsonic project's own spec, so don't assume
another OpenSubsonic server implements them just because it advertises
OpenSubsonic support in general.
| Extension | Versions | Endpoint(s) | Tested | Notes |
|---|---|---|---|---|
| `apiKeyAuthentication` | 1 | `tokenInfo`, `apiKey=` param on every endpoint | ✅ | Per-user named keys with plaintext shown once, SHA-256 hash stored, creation/last-used tracking, revocation (self or admin). `apiKey` conflicting with `u` returns error 43; an invalid/revoked key returns error 44. Managed at Settings → API Keys; admins see/revoke everyone's keys at `/admin/api-keys`. |