# Security notes What Minstrel does to protect accounts, and the reasoning behind the decisions that look odd at first. For deployment steps, see [hosting](./hosting.md). ## Accounts and sessions - **Passwords** are stored as bcrypt hashes. - **Login, registration and password reset are rate-limited:** 10 failed sign-ins per account and 50 per address per 15 minutes, with similar limits on register, forgot-password and reset. The Subsonic `/rest` API shares the login limits. An unknown username takes as long to reject as a wrong password, so timing doesn't reveal which accounts exist. - **Sessions** are random 256-bit tokens; the server stores only their SHA-256. A session ends after 30 days unused or 365 days in total. Changing your password signs out your other devices; a password reset, or an admin resetting it, signs out all of them. - **API keys** (the OpenSubsonic `apiKey`) are stored as SHA-256 too, so a key is shown once, when you generate it in Settings, and can only be replaced after that. - **The first account** on an empty server needs the setup token from the server log (see [hosting](./hosting.md#first-account)). - **Password-reset links** are built only from the configured public address, never from the request's `Host` header. ## Requests - Request bodies are capped at 4 MiB and must arrive within 30 seconds. Streams and the live-event connection are unaffected. - Every response carries `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, a restrictive `Permissions-Policy` and `X-Frame-Options: DENY`. The web app also gets a `Content-Security-Policy` that allows only its own scripts, by hash. - Media responses are `Cache-Control: private`, so a shared cache never keeps one user's audio or artwork for another. ## CSRF: SameSite cookies plus JSON-only writes There are no CSRF tokens. The session cookie is `SameSite=Strict`, so browsers don't send it with requests that start on another site. That leaves one gap: SameSite treats every subdomain of the same registrable domain as the same site, so a different app on `other.example.com` could still send a request carrying the cookie. To close it, any state-changing `/api` request authenticated by the cookie must have a JSON body (`Content-Type: application/json`) or no body at all. An HTML form or a script on another origin can't send JSON without a CORS preflight, and Minstrel answers no cross-origin preflight. Requests authenticated with a bearer token or the Subsonic query parameters carry nothing a browser attaches on its own, so they aren't checked. ## Subsonic sign-in, and the one password stored in plain text Classic Subsonic clients sign in with `t` and `s`: the MD5 of the password followed by a random salt. To check that, the server has to know the password itself, so supporting this sign-in method means storing a password Minstrel can read. That is the Subsonic password, and it is the only credential Minstrel keeps unhashed. - **The recommended way in is the API key.** Clients that support the OpenSubsonic `apiKey` should use it. The key is stored hashed and can be replaced at any time in Settings. - **The Subsonic password is never your login password.** Minstrel generates it (**Settings → Subsonic password**), shows it once, and lets you replace or turn it off. Because it is random, a copy of the database exposes access to this server's Subsonic API and nothing else: it can't be a password you also use somewhere else. - **`t`/`s` and `p=` sign-in are off for an account until it has a Subsonic password.** Plain `p=` sign-in is additionally off server-wide unless `subsonic.allow_plaintext_password` is enabled. - `minstrel admin reset-password` changes only the login password. Older versions also copied it into the Subsonic password; upgrading clears every Subsonic password once, so those copies are gone. An account that used `t`/`s` sign-in needs a new Subsonic password generated in Settings. ## Android allows plain HTTP The Android app permits cleartext connections, for two reasons that can't be narrowed to a list of hosts. The server address is whatever the user types, and many self-hosted servers run plain HTTP on a LAN. And UPnP, DLNA and Sonos speakers are controlled over plain HTTP at addresses that are only known once they're discovered. The reasoning lives in `android/app/src/main/res/xml/network_security_config.xml`. This doesn't weaken app updates: an APK altered in transit fails the platform's signature check. It does mean a server reached over plain HTTP across the internet exposes the session cookie in transit, which is why the [hosting guide](./hosting.md) puts public servers behind HTTPS. On the phone, the session cookie is encrypted with a key held in the Android Keystore, so a copy of the app's files doesn't yield a usable session. ## Build pipeline Nothing is published unless every check passes: the Go, integration, web and Android test suites, `govulncheck` against the toolchain that builds the image, and `npm audit` on the packages that ship to the browser. See `.gitea/workflows/release.yml`. ## Reporting a problem Open an issue on the [repository](https://git.fabledsword.com/bvandeusen/minstrel/issues), or contact the maintainer privately first if it's something that shouldn't be public until fixed.