# Security Policy ## Supported Versions Only the latest released version of EverShelf receives security fixes. | Version | Supported | |---------|-----------| | Latest (1.7.x) | ✅ | | Older releases | ❌ | ## Reporting a Vulnerability **Please do NOT open a public GitHub issue for security vulnerabilities.** Report security issues privately via email: **📧 evershelfproject@gmail.com** Include: - A description of the vulnerability - Steps to reproduce - Potential impact - Your GitHub username (optional — for credit) I aim to acknowledge reports within **48 hours** and release a fix within **7 days** for critical issues. ## Scope EverShelf is a **self-hosted** application. The security model assumes: - It runs on a trusted private network (home LAN) - Access from the internet requires the user to set up their own authentication layer (e.g. reverse proxy with Authelia, Nginx `auth_basic`) Out-of-scope issues: - Vulnerabilities that require physical access to the server - Issues only affecting users who have not followed the security recommendations in the README - Denial-of-service attacks on the demo server ## Security Features - API keys stored server-side in `.env`, never sent to the browser - `get_settings` returns only boolean flags (`gemini_key_set`), never raw key values - Optional `API_TOKEN` (legacy alias `SETTINGS_TOKEN`) protects every data read and write (`hash_equals` to prevent timing attacks). `DEMO_MODE=true` blocks all write operations at the router level - **Token bootstrap requires pairing.** `app_bootstrap` never returns `API_TOKEN` to an anonymous request: the UI shows a dialog and the user types the one-time code. The code is shown on an already-paired device under **Settings → System → Security** (and Info), and is also printed in the server log (`grep -i "pairing code" logs/evershelf_*.log` or `docker logs evershelf 2>&1 | grep -i "pairing code"`, 30 min TTL, brute-force limited). The authenticated `pairing_code` action never returns the code to anonymous clients. If the `grep` returns nothing, the web user cannot write the log file — a log created by a root `cron` run is root-owned and silently rejects Apache's writes; run `scripts/fix-permissions.sh`. `API_BOOTSTRAP_OPEN=true` restores the old "trust any same-origin-looking request" behaviour and should only be used on a fully trusted LAN — the headers it relies on are client-controlled. - The calendar feed's credential is **read-only and lives in the URL, by design.** A calendar client can only GET a URL — it cannot send `X-API-Token`, and it will keep calling that URL for years — so `calendar_ics` is a public action guarded by its own `ICS_TOKEN` (`?token=…`, `hash_equals`, never logged) instead of the API token. A leaked subscription can therefore read the expiry list but cannot add, edit or delete anything, and *Rotate link* in Settings revokes it immediately. Rotation is also what makes the trade-off acceptable: the URL is a bearer credential, so it must be treated like a password (HTTPS, no screenshots, no third-party QR services — the pairing code is short-lived, this one is not). Rejected requests log only `ics_feed_unauthorized`, never the token itself. - **No authorisation decision is made from client-controlled headers.** Actions that run `docker` or rewrite `.env` (`mealie_install`, `mealie_configure`, …) and the scale gateway endpoints require the API token; the previous `Sec-Fetch-Site` / `Origin` bypass was removed. - **Every POST is checked, and the check no longer accepts a header a form can set.** The CSRF guard ran against a hand-written list of 25 actions out of 134 and treated `Content-Type: application/json` as proof of good faith, so a cross-site `