Seven security issues from an audit, plus album pages and the map coming
up empty against Immich 3.x. Verified against a real Immich 3.0.3
instance in a production build.
Security:
- Admin session forgery via a hardcoded 'dev-fallback-secret' signing key
that bypassed the production AUTH_SECRET guard
- /api/map ignored album passwords and returned working asset tokens;
rewriting ?size=thumbnail to ?size=original downloaded full-resolution
originals from protected albums
- Admin login had no rate limit, and the client IP came from a spoofable
header, so rotating it bypassed every limit in the app
- Raw Immich asset UUIDs were serialized into the RSC payload
- Album tokens never expired server-side; a captured token replayed forever
- Path traversal into the Immich API via an unvalidated album id
- /admin ran with no enforced CSP (only Report-Only reached it)
TRUSTED_PROXIES is replaced by TRUSTED_PROXY_HOPS. The old variable
matched proxy IPs against the socket peer address, which self-hosted
Next.js never exposes, so the check could never pass: unset, the limiter
read a spoofable header; set, every request shared one bucket and a single
client could rate-limit the whole site. Migrates automatically.
Immich 3.x: GET /albums/:id no longer embeds assets. Assets now come from
POST /search/metadata with withExif: true — without that flag EXIF is
omitted and grid aspect ratios, the lightbox panel and map markers stay
empty. Drops support for Immich below 3.0, which now logs a warning.
Upgrade: requires Immich 3.0+; set TRUSTED_PROXY_HOPS (nginx = 1);
visitors of protected areas and admin sessions must log in once more.
91 unit tests (25 new); the new admin-auth and rate-limit cases were
confirmed to fail against the pre-fix code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sanitized the Content-Type header on image and video proxy routes (`/api/image/*`, `/api/video/*`). The content type is forced to lowercase, validated against a safe prefix (`image/`, `video/`, `audio/`), and restricted to block `svg` and `xml`. Any invalid type falls back to `application/octet-stream`. This prevents an attacker from uploading an SVG file containing a malicious script and triggering a Stored XSS attack via these proxy endpoints, which are excluded from the global CSP.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Update in-memory rate limiter to fail secure (reject new requests) when maximum capacity is reached, rather than evicting the oldest tracking entry. This prevents an attacker from bypassing their rate limit by flooding the store with spoofed `x-forwarded-for` IPs to intentionally trigger cache eviction. Also cleaned up temporary files and updated tests to verify the behavior.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Upgraded `encodeAssetId` and `decodeAssetId` to use `aes-256-gcm` (Authenticated Encryption) instead of `aes-256-cbc`.
AES-CBC lacks authentication, leaving it potentially susceptible to padding oracle attacks or ciphertext manipulation.
The new token adds a `v2:` prefix which strictly enforces AES-GCM while supporting legacy fallback for cached urls.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- escapeHtml() in MapView.tsx sanitises city/country/url/name before HTML injection
- HeroTextContent extracted to top-level component with explicit props (was nested inside async Server Component)
- rate-limit.test.ts: add evictIfFull stress test (6/6 pass)
- Remove dead theme preset (no CSS, not in DevToolbar)
- Extract from closure → module-level export in lib/config/index.ts
- Extract helper in app/[...path]/page.tsx to deduplicate password gate check
- Extract inner component in app/page.tsx (was 4× inline in minimal/fullbleed/mosaic/split)
- Add 8 unit tests for buildSubpageGrid covering undefined, subset fields, layout fallback, and all fields
- Extract EXIF and Swipe logic into custom hooks for Lightbox
- Extract Map layout data formatting into mapService
- Extract AlbumDetailView and SubpageGridView components from page router
- Restructure monolithic config.ts into clean config/ directory