- Never hardcode secrets in any file committed to the repository.
- All secrets must be in environment variables loaded at runtime.
- HTTPS only in production. HTTP strictly for local development.
- All Etsy tokens must be encrypted at rest in the database.
- JWT access tokens expire in 15 minutes. Refresh tokens expire in 7 days.
- Refresh tokens are rotated on every use.
- Token blacklist maintained in Redis for logout and revocation.
- Stripe webhook signatures must always be verified.
- All admin routes must verify
role=adminon every request. - No PII or credentials may appear in logs.
- All user inputs must be validated with Pydantic schemas.
- SQL queries must use ORM parameterization — no raw string interpolation.
- File uploads must validate type and size before accepting.
- CORS must be configured to allow only known frontend origins.
- Rate limiting must be applied to all public endpoints.
- All external write operations must be logged in the audit log.
| Risk | Mitigation |
|---|---|
| A01 Broken Access Control | RBAC on all routes, organization-scoped queries |
| A02 Cryptographic Failures | Bcrypt passwords, encrypted Etsy tokens, HTTPS |
| A03 Injection | SQLAlchemy ORM, Pydantic validation |
| A04 Insecure Design | Preview-before-write, safe external write protocol |
| A05 Security Misconfiguration | Env vars only, no debug mode in prod |
| A06 Vulnerable Components | Dependabot alerts, regular dependency updates |
| A07 Auth Failures | JWT rotation, short expiry, Redis blacklist |
| A08 Software/Data Integrity | Webhook signature verification |
| A09 Logging Failures | Structured logging, no PII in logs, audit trail |
| A10 SSRF | Whitelist allowed external URLs, no user-controlled URLs |
- All 11 tested protected endpoints return 401/403 without a valid JWT
- JWT tampering (corrupted signature) correctly returns 401
- Superuser gate enforced on all 4 admin endpoints tested (403 for regular users)
- No
password_hashin any user response - No
access_token_encor Etsy tokens in any shop response - No Stripe secrets in any billing response
- Org isolation confirmed: 6 resource types scope correctly to requesting organization
- SQL injection in
title,tag,sort_byquery params: returns 422 or empty list, never 500 - Path traversal and overlong IDs return 404/422, never 500
- Python stack traces not exposed in error responses
- FastAPI
HTTPBearerreturns 403 (not 401) for missing Authorization header — RFC 7235 expects 401 for unauthenticated. Fix: configureHTTPBearer(auto_error=False)and raise 401 manually. - No Sentry DSN configured locally — configure in staging/production.
app/core/rate_limit.py— in-memory rate limiter.RATE_LIMIT_ENABLED=falseby default (enabled in production).- Login: 10 attempts/min per IP (
POST /api/v1/auth/login). - Register: 5 attempts/min per IP (
POST /api/v1/auth/register). - Returns HTTP 429 +
{"detail": "Too many requests. Please try again later."}+Retry-Afterheader. - Production: set
RATE_LIMIT_ENABLED=trueandRATE_LIMIT_BACKEND=redis. Redis-backed implementation deferred to Sprint 21.
All API responses now include via SecurityHeadersMiddleware:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera=(), microphone=(), geolocation=()
All frontend routes include via next.config.mjs headers config:
Content-Security-Policy— default-src 'self'; script-src includes 'unsafe-inline' (see note below)X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self)X-DNS-Prefetch-Control: on
CSP 'unsafe-inline' Note: Required by the anti-flash theme script injected via dangerouslySetInnerHTML in app/layout.tsx. This is a known weakness. Sprint 21 will implement nonce-based CSP to remove 'unsafe-inline'.
- All CI jobs use dummy/placeholder secrets — never real keys.
RATE_LIMIT_ENABLED=falsein CI to prevent 429 test flakiness.- No
.envfiles or.local-superusers.envcommitted or used in CI.
LocalUploadPanelis frontend-only. No file is sent to any server.- Object URLs (
blob:) are scoped to the current tab and revoked on file removal and component unmount. - Dual validation: MIME type (
file.type) AND extension (file.namesuffix). Rejects files where either check fails. - Max file size: 10 MB. Max count: 20. Rejected files show user-facing error messages, never auto-processed.
- No path traversal risk — no filesystem write path involved.
- Real credentials for local demo users go in
apps/backend/.local-superusers.envonly. - That file is gitignored (
*.envpattern + explicit paths) and must never be committed. - The example file
apps/backend/.local-superusers.env.examplecontains fake placeholder values only. - The seed script never prints passwords and never logs secrets.
- No Stripe calls are made — subscription records are created directly in DB (local dev only).
- Seeded users are marked
is_superuser=Truefor local dev access only. - Do not use the same credentials in any other environment.
| Date | Severity | Finding | Status |
|---|---|---|---|
| — | — | No findings yet | — |
- Run automated OWASP ZAP scan
- Run dependency vulnerability audit (
pip audit,npm audit) - Review all JWT handling
- Review all Stripe webhook handling
- Review CORS and CSP headers
- Review rate limiting coverage
- Review file upload validation
- Review admin route protection
- Review audit log completeness
- Penetration test checklist