Threat model
This model names the assets to protect and the controls this design requires. It is the security review checklist for implementation. It is not a procedure for attacking the system.
Assets
| Asset | Where it lives | Impact if disclosed or changed |
|---|---|---|
| Customer identity and tokens | Hashed refresh tokens and short-lived access tokens | Account takeover. |
| USD payments | Stripe and the payments table | Wrong charges or missed refunds. |
| Photographs | Private bucket | Private images become public. |
| Gallery access decision | galleries.access_status | A customer sees files the photographer did not approve. |
| Reward ledger | reward_ledger | Discounts are forged or erased. |
| Administrator actions | audit_logs | A decision cannot be reconstructed. |
| Provider and storage secrets | Environment or a secret manager | Forged checkouts or direct bucket access. |
Actors
| Actor | Trust |
|---|---|
| Visitor | None. Can call public routes. |
| Customer | Authenticated for their own records. |
| Photographer | Trusted to decide bookings and access. Still authenticated, with MFA, and audited. |
| Stripe | Trusted for payment events only after the signature check. |
| Operator | Trusted to deploy and restore. Uses a separate database role. |
Threats and required controls
| Area | Threat | Required control |
|---|---|---|
| Authentication | Stolen password database | Argon2id hashes. No reversible passwords. |
| Authentication | Stolen refresh token | Refresh token stored only as a hash. Rotation on use. Logout revocation. |
| Authorization | Customer reads another customer's gallery | Server-side owner filter on every request. |
| Gallery approval | Published gallery treated as public | Access status checked on every view and download. |
| Image storage | Bucket listing or permanent public URL | Private bucket. Signed URL lifetime of 5 minutes. Access checked before signing. |
| Image storage | Revoked user keeps a URL | Accepted until expiry. Short lifetime limits the window. New signatures stop at revocation. |
| Payments | Forged webhook marks an order paid | Verify the Stripe signature before any write. |
| Payments | Duplicate webhook double-credits a reward | UNIQUE(provider, event_id) in the same transaction as the state change. |
| Payments | Success redirect treated as payment | Order stays unpaid until a verified webhook or provider read. |
| API | Malformed amounts and ids | Validate input. Prices come from service_packages. |
| API | Stolen access token used from another client | 15-minute access-token lifetime. Refresh tokens are rotated and can be revoked. |
| API | Credential stuffing | Rate limits on login, setup, reset, and checkout. |
| Uploads | Disguised file content | Magic-byte checks, 50 MB limit, and worker processing before READY. |
| Referrals | Self-referral or a second reward | Check constraint, unique referred user, unique qualifying order, unique reward pair. |
| Referrals | Two checkouts spend one balance | User-row lock and one ledger transaction. |
| Referrals | History rewritten after a refund | Append-only ledger. Corrections are reversals. |
| Database | Network sniffing or an over-privileged app | TLS on DATABASE_URL. The database user is not a superuser. Queries go through the generated Prisma Client. Backups encrypted at rest. |
| Administration | Unattended administrator token | MFA before the access token is issued. Audit log append-only. |
| Secrets | Secrets committed to the repo | Environment variables or a secret manager. Seed data contains no live secrets. |
Residual risks to accept explicitly
- A signed URL works until it expires, including after revocation. The 5-minute lifetime is the chosen bound.
- The photographer is a powerful actor. MFA and audit logs record decisions. They do not prevent a legitimate administrator from approving a gallery.
- Email delivery can fail. The in-app notification and the delivery row remain, and the worker retries. A customer who never receives setup mail uses the password-recovery path after an operator confirms the address, or a new setup token is issued.
- Webhook delay leaves an order unpaid after the customer has paid at the provider. Reconciliation reads the provider. It does not trust the browser.
Review gate
Implementation of payments, gallery downloads, and reward redemption is complete for security purposes only when the acceptance criteria AC-6, AC-8, AC-12 through AC-16, and AC-19 through AC-24 pass in automated tests.