CodeVetter · Repo Unpack · Risk map
immich-app/immich
Security-sensitive surface concentrates on file upload/disk IO, auth token handling, subprocess media tooling, and the WASM plugin host. The multer storage engine writes user-supplied bytes to predictable uuid paths, ffmpeg/exiftool process untrusted media, and extism plugins run third-party WASM gated by env vars. Operational risks include a 24h request timeout, trust-proxy assumptions, startup-time migrations under an advisory lock, and a supervisor that kills the whole process on worker failure. Several safety rails exist (mandatory @Authenticated, duplicate-job startup validation, constant-time login compare) but are themselves fragile coupling points.
- File upload writes untrusted bytes through a custom multer _handleFile to disk paths derived from StorageCore folders; filenames are sanitized via sanitize-filename and uuid-based, but the interceptor runs before controller validation, so malformed multipart payloads hit the FS layer early.
- express 'trust proxy' is set to ['loopback', ...network.trustedProxies] from IMMICH_TRUSTED_PROXIES; clientIp used in login logging/GetLoginDetails depends on this being correct — misconfiguration enables IP spoofing in audit logs and rate-sensitive paths.
- server.requestTimeout is set to 24 hours (24*60*60*1000) to support huge uploads/downloads; combined with 10mb JSON body limits this leaves long-lived socket exposure on the API port.
- Password auth uses bcrypt compare against LOGIN_DUMMY_HASH when the email is unknown (constant-time anti-enumeration) — the dummy hash constant lives in constants.ts and weakening/removing it would reopen user enumeration.
- Shared-link access is authorized via query parameters (key/slug) declared with ApiQuery in the @Authenticated decorator, meaning bearer-equivalent secrets can appear in URLs, access logs, and referrers. (inferred)
- Media processing shells out to ffmpeg/ffprobe (fluent-ffmpeg) and exiftool-vendored on user-uploaded files; media.repository.ts builds ffmpeg calls with niceness 10 — a large blast radius if argument handling or the bundled jellyfin-ffmpeg is compromised.
- The plugin host instantiates extism WASM plugins pooled via generic-pool and calls plugin methods by name; external plugin installation is env-gated (IMMICH_ALLOW_EXTERNAL_PLUGINS) and mis-set values expand supply-chain surface.
- Startup is stateful and lock-coupled: the supervisor busy-polls pg_try_advisory_lock(MaintenanceOperation), DatabaseService runs migrations under DatabaseLock.Migrations, and nightly jobs require DatabaseLock.NightlyJobs — stuck locks or split-brain DBs can wedge boot or scheduling.
- Any non-zero worker exit kills the API child (SIGTERM) and exits the supervisor with that code — a crash in a background job can take down the whole API unless orchestrated restart policies compensate.
- Every route must declare @Authenticated() or AuthGuard throws at request time ('Route X does not declare @Authenticated()'); this is a strong default-deny but means a missing decorator yields runtime 500s rather than compile errors.
- Library watching uses chokidar over admin-configured filesystem paths and writes discovered files into the managed tree; a hostile or malformed library path expands file-IO reach beyond the media root.
- Dev/test convenience defaults embed credentials: DB_URL defaults to postgres://postgres:postgres@localhost:5432/immich in migration scripts and sync-open-api.ts hardcodes the same DSN — fine for local dev but a footgun if reused.
- Postgres's 65535 bind-parameter limit is worked around via the @Chunked decorator (DATABASE_PARAMETER_CHUNK_SIZE 65_500); repository methods handling ID sets rely on authors remembering to decorate — a missed site yields runtime driver errors on large batches.
- Mobile pins a git dependency (native_video_player) to a commit ref and consumes a generated openapi package — both are supply-chain and drift-sensitive integration points. (inferred)