Introduces the SDD root: a v1.0.0 constitution and machine-readable AGENTS.md grounded in the project's real conventions; six EARS-aware persona spec-review checklists that cross-reference .claude/personas/; feature-spec/ADR/threat-model/ api-contract templates; a fully worked _example feature; a living RTM; and an adrs/ pointer that reuses the existing docs/adr/ archive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 lines
2.8 KiB
Markdown
46 lines
2.8 KiB
Markdown
# 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.
|