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>
2.8 KiB
2.8 KiB
Threat Model — Profile picture upload
Feature spec: ./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 atavatars/{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 |
|---|---|---|---|---|---|
| Spoofing | F-1 | Unauthenticated caller uploads/deletes an avatar | Session auth required; @RequirePermission (REQ-006) |
Low × Med | Mitigated |
| Tampering | 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 |
| Repudiation | 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 |
| Information 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 |
| Information 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 |
| Denial 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 |
| Elevation 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.