Add notification image proxy, expand README, fix compose env-file handling

- Proxy notification image_url server-side (GET /api/v1/notifications/:id/image)
  instead of loading it directly in the browser, so LAN-only image sources
  (e.g. Frigate snapshots) still load for clients off that network
- Tighten CSP img-src to 'self' now that images are same-origin
- Update threat model / architecture docs to match the new image-fetch design
- Expand README: Home Assistant quick-start, Pocket ID/OIDC walkthrough,
  Pangolin note, backup automation, new troubleshooting entries
- Fix docker-compose.yml: document that --env-file .env must be passed
  explicitly (Compose's implicit .env lookup was unreliable with -f from
  a different directory)
- Fix image-proxy tests to build a real session cookie (AUTH_MODE=oidc
  applies inside the test container too, so requireAuth needs one)
This commit is contained in:
siliconsolder committed 2026-09-07 18:49:45 +00:00
1 parent a82c29a69d
commit 079013730b
10 files changed
+332 -32

No files matched your search

+26 -6
View File
@@ -72,12 +72,32 @@ is still gated by needing a valid token.
**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 `<a href>` / `<img src>` 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).
**Mitigation, `url`:** never fetched server-side. Stored as an opaque
string and only ever rendered as `<a href>` in the *browser's* context,
exactly like a normal webpage.
**Mitigation, `image_url`:** unlike `url`, this **is** fetched server-side
— `GET /api/v1/notifications/:id/image` — so that a LAN-only image source
(a camera snapshot, say) still loads for a client that's off that LAN,
since the server sits on it persistently; the browser is deliberately kept
from ever needing to reach an arbitrary third-party host itself (also
closes a tracking-pixel-style privacy leak the pure client-side-load
approach had). This is a narrower, more deliberate SSRF surface than a
general-purpose fetch proxy, not the absence of one:
- No client-supplied URL parameter — only the fixed value already stored
on a notification, and only after confirming the caller can see that
notification (same topic-visibility check as reading it at all).
- Setting that value in the first place requires a valid, scoped API
token — the threat actor already needs ingestion access, same
prerequisite as any other publish-time attack in this model.
- Request has a 10s timeout, a 20MB response-size cap, and the response is
only ever forwarded back to the same authorized viewer as opaque image
bytes (never reflected into a response an attacker could read directly,
and rejected outright if the upstream `Content-Type` isn't `image/*`).
- Residual risk: a scoped token holder could still use this as a blind
probe (timing/success of the fetch) against internal hosts the server
can reach. Accepted for a single-tenant homelab deployment where
publishing already requires a trusted, scoped credential; revisit if
Notizarr is ever exposed to less-trusted publishers.
### XSS via notification content
**Threat:** `title`/`message` containing `<script>` or event handlers.