diff --git a/PROGRESS.md b/PROGRESS.md index f6f4185..83aaa30 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -151,3 +151,4 @@ All plans in [docs/plans/](docs/plans/). Completed plans are kept as architectur | [notification-overhaul.md](docs/plans/notification-overhaul.md) | Planned | | [live-site-editor.md](docs/plans/live-site-editor.md) | Design exploration | | [profit-aware-pricing.md](docs/plans/profit-aware-pricing.md) | Planned | +| [security-hardening.md](docs/plans/security-hardening.md) | Planned | diff --git a/config/runtime.exs b/config/runtime.exs index f95a858..477fff0 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -64,7 +64,9 @@ if config_env() == :prod do ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: port ], - secret_key_base: secret_key_base + secret_key_base: secret_key_base, + # HSTS tells browsers to always use HTTPS for this domain (1 year, include subdomains) + force_ssl: [hsts: true, rewrite_on: [:x_forwarded_proto]] # ## SSL Support # diff --git a/docs/plans/security-hardening.md b/docs/plans/security-hardening.md new file mode 100644 index 0000000..7dc8854 --- /dev/null +++ b/docs/plans/security-hardening.md @@ -0,0 +1,247 @@ +# Security hardening + +Status: Planned + +## Context + +Berrypod is designed as a simpler, safer alternative to WordPress/WooCommerce for print-on-demand sellers. Compared to WordPress, it has significant architectural advantages: + +- No plugin ecosystem = no plugin vulnerabilities +- Compiled Elixir binary = no code injection +- Image uploads stored as database BLOBs = no filesystem execution risk +- Ecto changesets = parameterised queries, SQL injection structurally prevented +- Built-in CSRF via Phoenix = consistent protection +- Encrypted secrets in database = no plaintext credentials + +However, to make security a genuine selling point, several gaps need addressing. This plan covers three phases of hardening to bring Berrypod to best-in-class security. + +## Current state (audit summary) + +| Area | Current | Gap | +|------|---------|-----| +| Password hashing | Bcrypt with timing attack prevention | OK | +| Session management | Token-based, 7-day rotation, signed cookies | OK | +| Magic links | SHA256 hashed, 15-min expiry | OK | +| 2FA | None | **Critical gap** | +| Rate limiting | None | **Critical gap** | +| HSTS | Commented out in config | **Should enable** | +| CSP | Basic (`frame-ancestors`, `base-uri` only) | Could be stronger | +| SVG uploads | Stored safely, recolored via validated params | Minor XSS risk | +| Audit logging | Activity log exists but not comprehensive | Enhancement | +| Session encryption | Signed but not encrypted | Enhancement | + +## Design + +### Phase 1: Essentials + +**1.1 Rate limiting** + +Add rate limiting using the `hammer` library. Apply to: + +- Login attempts: 5 per minute per IP +- Magic link requests: 3 per minute per email +- API endpoints: 60 per minute per IP +- Newsletter signup: 10 per minute per IP + +Implementation via a `RateLimitPlug` that checks `Hammer.check_rate/3` and returns 429 on breach. + +**1.2 HSTS headers** + +Enable `force_ssl` in production config with HSTS headers: + +```elixir +config :berrypod, BerrypodWeb.Endpoint, + force_ssl: [hsts: true, rewrite_on: [:x_forwarded_proto]] +``` + +The `rewrite_on` handles Fly.io's SSL termination at the proxy. + +**1.3 Two-factor authentication (TOTP)** + +Add TOTP-based 2FA for the admin account: + +- New schema: `user_totp` (secret, enabled_at, backup_codes) +- Setup flow: show QR code, verify code, generate backup codes +- Login flow: after password, prompt for TOTP code +- Backup codes: 8 single-use codes, stored hashed +- Recovery: use backup code to disable 2FA and re-setup + +Libraries: `nimble_totp` for TOTP generation/verification. + +### Phase 2: Hardening + +**2.1 Comprehensive CSP** + +Extend CSP headers to restrict script/style sources: + +``` +default-src 'self'; +script-src 'self'; +style-src 'self' 'unsafe-inline'; +img-src 'self' data: blob:; +font-src 'self'; +connect-src 'self' wss:; +frame-ancestors 'self'; +base-uri 'self'; +form-action 'self'; +``` + +Note: `'unsafe-inline'` for styles is required because we inject theme CSS variables dynamically. Could use nonces but adds complexity. + +**2.2 SVG sanitisation** + +When uploading SVGs, strip potentially dangerous elements: + +- Remove `