# 02 — Threat Model ## Trust domains | Domain | Trust level | Notes | |---|---|---| | A. Public internet (browsers) | Untrusted until OIDC-authenticated | Only reaches listener A, via Pangolin | | B. Pangolin reverse proxy | Trusted **only** when its source IP is in `TRUSTED_PROXY_CIDRS` and `AUTH_MODE=proxy` | Otherwise treated as domain A | | C. LAN services (Home Assistant, Seerr, etc.) | Semi-trusted network, but the real boundary is the API token | Reaches listener B only | | D. notizarr app | Mediates every access | | | E. Postgres | Fully trusted, but only reachable from the notizarr container on the internal Docker network | Never published to host | ## Assets to protect - Notification history (may contain home automation state, personal info) - API tokens (plaintext shown once, hash stored) - OIDC client secret - VAPID private key - Push subscription endpoints (identify a user's device to a push service) - Session cookies - Admin functions (user/topic/token management) ## Threats and mitigations ### Spoofed identity via proxy headers **Threat:** an attacker on the LAN or a compromised container sends `X-Auth-Request-Email` directly to notizarr, bypassing Pangolin. **Mitigation:** proxy-header auth is only trusted when the *direct TCP peer* address (not a forwarded-for chain) is inside `TRUSTED_PROXY_CIDRS`. If `AUTH_MODE=oidc`, proxy headers are never read at all. Document that Pangolin must connect directly to notizarr (no additional untrusted hop in between). ### Forged/leaked OIDC tokens **Threat:** token replay, audience confusion, state/nonce fixation. **Mitigation:** Authorization Code + PKCE (S256) via `openid-client`; validate `iss`, `aud`, `nonce`, `exp`, signature against the issuer's JWKS; state/nonce stored in a short-lived signed httpOnly cookie scoped to the auth flow only. ### Session hijacking / CSRF **Threat:** stolen session cookie, or a malicious site making authenticated requests on the user's behalf. **Mitigation:** `httpOnly`, `Secure`, `SameSite=Lax` session cookie. All mutating requests (`POST`/`PATCH`/`DELETE` on listener A) additionally require a custom header (`X-Requested-With: notizarr`) and are rejected if the `Origin` header doesn't match `PUBLIC_BASE_URL` — a cross-site form post can't set custom headers, so this blocks CSRF without needing a token handshake. ### API token theft / abuse **Threat:** a leaked LAN token is used to flood topics, read data it shouldn't, or pivot into admin functions. **Mitigation:** tokens are write-only credentials — they can **only** hit listener B's publish endpoints, never any read/admin endpoint on listener A, even if presented there. Tokens are scoped (topics, source name, source CIDR, expiry). Only a SHA-256 hash is stored; plaintext is shown once at creation/rotation. Per-token rate limiting caps blast radius. Revocation is immediate (checked on every request, no caching of revoked status). ### Ingestion listener exposed to the internet by misconfiguration **Threat:** admin accidentally publishes 8081 to `0.0.0.0` on a port-forwarded router, or Pangolin is misrouted to it. **Mitigation:** defense in depth beyond "the admin will firewall it": optional `INGESTION_ALLOWED_CIDRS` env var enforced in-app (default: RFC1918 ranges) rejects any publish request from outside those CIDRs regardless of token validity. Listener B never serves the SPA or session auth, so even a fully exposed 8081 can't be used to browse notification history or reach admin functions — worst case is unauthorized token-scoped publishing, which is still gated by needing a valid token. ### SSRF via notification content **Threat:** a malicious/compromised publisher sets `image_url` or `url` to an internal address (`http://169.254.169.254/...`, `http://postgres:5432`, etc.), hoping the server fetches it. **Mitigation:** the server **never fetches these URLs**. They are stored as opaque strings and only ever rendered as `` / `` in the *browser's* context, exactly like a normal webpage. This removes SSRF as a viable vector for this field entirely, at the cost of not offering server-side attachment proxying/caching (acceptable per the "attachment metadata only" requirement). ### XSS via notification content **Threat:** `title`/`message` containing `