# Threat Model — Profile picture upload **Feature spec:** [./spec.md](./spec.md) **Date:** 2026-06-13 **Author:** Security persona (worked example) ## Data Flow Diagram (text) **Actors** - Anonymous visitor (unauthenticated) - Authenticated user (uploads their own avatar) - Admin (`Permission.ADMIN_USER` — may remove others' avatars) **Trust boundaries** - TB-1: Browser ⇄ Caddy (public internet ⇄ DMZ) - TB-2: Caddy ⇄ Backend `:8080` (DMZ ⇄ app) - TB-3: Backend ⇄ MinIO + PostgreSQL (app ⇄ data plane) **Data flows** - F-1: Browser → [TB-1,TB-2] → `UserAvatarController` : multipart image - F-2: `UserService` → [TB-3] → MinIO : object at `avatars/{userId}` - F-3: `UserService` → [TB-3] → PostgreSQL : `app_users.avatar_object_key` - F-4: Browser → [TB-1,TB-2,TB-3] → MinIO (via proxy GET) : image bytes ## STRIDE | Threat Category | Asset / Flow | Threat Description | Mitigation | Likelihood × Impact | Status | |---|---|---|---|---|---| | **S**poofing | F-1 | Unauthenticated caller uploads/deletes an avatar | Session auth required; `@RequirePermission` (REQ-006) | Low × Med | Mitigated | | **T**ampering | F-3 | Caller sets `avatarObjectKey` via request body to point at an arbitrary stored object | `avatarObjectKey` is server-set in `UserService` only, never bound from body (CWE-639) | Med × High | Mitigated | | **R**epudiation | F-2/F-3 | No record of who changed an avatar | Standard request logging by user UUID (no PII); admin deletions auditable via existing logs | Low × Low | Accepted | | **I**nformation disclosure | F-4 | A public/signed S3 URL would let anyone fetch any avatar without auth | Avatars served only through the authenticated proxy `GET /api/users/{id}/avatar`; no public URL | Med × Med | Mitigated | | **I**nformation disclosure | F-1 | Malicious file (polyglot) served back with a sniffed content type → stored XSS | Store with a fixed `image/png`/`image/jpeg` content type; proxy sets `Content-Type` + `X-Content-Type-Options: nosniff`; only PNG/JPEG accepted (REQ-007) | Low × High | Mitigated | | **D**enial of service | F-1/F-2 | Oversized or many uploads exhaust storage/memory | 2 MB cap enforced before MinIO write + `multipart.max-file-size` ceiling (REQ-008); deterministic key means one object per user | Med × Med | Mitigated | | **E**levation of privilege | F-1 | Non-admin removes/replaces another user's avatar via `/{id}` | Ownership check; `ADMIN_USER` required for `/{id}` (REQ-005/REQ-009, 403) | Low × Med | Mitigated | ## ASTRIDE Not applicable — this feature invokes no AI agent, model, or tool. ## Residual Risk - **Repudiation (Accepted):** avatar changes are not written to a dedicated audit table. Accepted because the asset is low-value (a self-chosen picture) and request logs already attribute the action to a user UUID. Revisit if avatars ever become trust signals.