- Add Hammer library for rate limiting with ETS backend - Rate limit login (5/min), magic link (3/min), newsletter (10/min), API (60/min) - Add themed 429 error page using bare shop styling - Enable HSTS in production with rewrite_on for Fly proxy - Add security hardening plan to docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.6 KiB
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:
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
<script>tags - Remove
on*event handlers (onclick, onload, etc.) - Remove
<foreignObject>(can contain HTML) - Remove
javascript:URLs inhref/xlink:href
Use a simple regex-based sanitiser (SVGs are simple enough that a full parser is overkill). Run on upload, store sanitised version.
2.3 Comprehensive audit logging
Extend activity log to capture all admin actions:
| Event | Data |
|---|---|
| Admin login | IP, user agent, success/failure |
| Settings changed | Which setting, old/new value (secrets masked) |
| Theme changed | Which property |
| Page edited | Page slug, blocks changed |
| Order status changed | Order number, old/new status |
| Provider credentials updated | Provider name |
| Image uploaded/deleted | Image ID, filename |
Store in existing activity_log table. Add retention policy (90 days default).
2.4 Session encryption
Add encryption to session cookies:
@session_options [
store: :cookie,
key: "_berrypod_key",
signing_salt: "JNwRcD7y",
encryption_salt: "zKp4Q1vE", # Add this
same_site: "Lax",
max_age: 604_800
]
Sessions will be both signed (integrity) and encrypted (confidentiality).
Phase 3: Advanced (future)
Not in this plan but noted for future:
- WebAuthn/passkey support
- Automated security scanning in CI
- Penetration testing
- Database encryption at rest
- Login anomaly detection (geo, device fingerprint)
Changes
Phase 1
| File | Change |
|---|---|
mix.exs |
Add hammer, hammer_ets, nimble_totp deps |
lib/berrypod/application.ex |
Start Hammer supervisor |
lib/berrypod_web/plugs/rate_limit.ex |
New — rate limit plug |
lib/berrypod_web/router.ex |
Add rate limit plug to pipelines |
config/runtime.exs |
Enable force_ssl with HSTS |
lib/berrypod/accounts/user_totp.ex |
New — TOTP schema |
lib/berrypod/accounts.ex |
Add TOTP functions |
lib/berrypod_web/live/auth/totp_setup.ex |
New — 2FA setup LiveView |
lib/berrypod_web/live/auth/totp_verify.ex |
New — 2FA challenge LiveView |
lib/berrypod_web/user_auth.ex |
Integrate TOTP verification in login flow |
lib/berrypod_web/live/admin/settings.ex |
Add 2FA setup section |
| Migration | Add user_totp table |
| Tests | Rate limit plug, TOTP setup/verify, login with 2FA |
Phase 2
| File | Change |
|---|---|
lib/berrypod_web/plugs/content_security_policy.ex |
New — CSP plug |
lib/berrypod_web/router.ex |
Add CSP plug to browser pipeline |
lib/berrypod/media/svg_sanitiser.ex |
New — SVG sanitisation |
lib/berrypod/media.ex |
Call sanitiser on SVG upload |
lib/berrypod/activity_log.ex |
Add new event types |
lib/berrypod_web/endpoint.ex |
Add encryption_salt to session options |
| Tests | CSP headers, SVG sanitisation, audit events |
Tasks
Phase 1: Essentials
| # | Task | Est | Status |
|---|---|---|---|
| 1 | Add hammer and hammer_ets deps, start supervisor |
30m | planned |
| 2 | Create rate limit plug with configurable limits | 1h | planned |
| 3 | Apply rate limiting to login, magic link, API, newsletter | 1h | planned |
| 4 | Enable HSTS in production config | 15m | planned |
| 5 | Add nimble_totp dep, create user_totp schema and migration |
45m | planned |
| 6 | Add TOTP functions to Accounts context | 1h | planned |
| 7 | Create TOTP setup LiveView (QR code, verification, backup codes) | 2h | planned |
| 8 | Create TOTP verification LiveView (login challenge) | 1h | planned |
| 9 | Integrate TOTP into login flow | 1.5h | planned |
| 10 | Add 2FA section to admin settings page | 1h | planned |
| 11 | Tests for rate limiting | 1h | planned |
| 12 | Tests for TOTP setup and verification | 1.5h | planned |
Phase 2: Hardening
| # | Task | Est | Status |
|---|---|---|---|
| 13 | Create CSP plug with comprehensive policy | 1h | planned |
| 14 | Create SVG sanitiser module | 1.5h | planned |
| 15 | Integrate SVG sanitiser into upload flow | 30m | planned |
| 16 | Extend activity log with comprehensive admin events | 2h | planned |
| 17 | Add encryption_salt to session config | 15m | planned |
| 18 | Tests for CSP, SVG sanitiser, audit events | 2h | planned |
Total estimate: ~18 hours
Verification
Phase 1
- Rate limiting: Rapid login attempts return 429 after limit exceeded
- HSTS:
curl -Ishowsstrict-transport-securityheader in production - 2FA setup: QR code scans in authenticator app, codes verify
- 2FA login: After password, prompted for TOTP code
- Backup codes: Can disable 2FA using backup code
mix precommitpasses
Phase 2
- CSP: Browser dev tools show full CSP header on responses
- SVG sanitisation: Uploaded SVG with
<script>has it stripped - Audit log: Settings change shows in activity log with before/after
- Session encryption: Cookie is encrypted (not readable without secret)
mix precommitpasses
Security claims checklist
After completing this plan, Berrypod can legitimately claim:
- No plugin vulnerabilities (architectural)
- No filesystem code execution (architectural)
- SQL injection prevented (Ecto)
- CSRF protection built-in (Phoenix)
- Bcrypt password hashing
- Timing-attack-safe authentication
- Encrypted secrets storage
- Two-factor authentication (Phase 1)
- Rate limiting on auth endpoints (Phase 1)
- HSTS enforced (Phase 1)
- Comprehensive CSP headers (Phase 2)
- SVG upload sanitisation (Phase 2)
- Full admin audit trail (Phase 2)
- Encrypted session cookies (Phase 2)
This positions Berrypod as genuinely more secure than a typical WordPress/WooCommerce installation, with much lower ongoing maintenance burden.