248 lines
8.6 KiB
Markdown
248 lines
8.6 KiB
Markdown
|
|
# 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 `<script>` tags
|
||
|
|
- Remove `on*` event handlers (onclick, onload, etc.)
|
||
|
|
- Remove `<foreignObject>` (can contain HTML)
|
||
|
|
- Remove `javascript:` URLs in `href`/`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:
|
||
|
|
|
||
|
|
```elixir
|
||
|
|
@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
|
||
|
|
|
||
|
|
1. Rate limiting: Rapid login attempts return 429 after limit exceeded
|
||
|
|
2. HSTS: `curl -I` shows `strict-transport-security` header in production
|
||
|
|
3. 2FA setup: QR code scans in authenticator app, codes verify
|
||
|
|
4. 2FA login: After password, prompted for TOTP code
|
||
|
|
5. Backup codes: Can disable 2FA using backup code
|
||
|
|
6. `mix precommit` passes
|
||
|
|
|
||
|
|
### Phase 2
|
||
|
|
|
||
|
|
1. CSP: Browser dev tools show full CSP header on responses
|
||
|
|
2. SVG sanitisation: Uploaded SVG with `<script>` has it stripped
|
||
|
|
3. Audit log: Settings change shows in activity log with before/after
|
||
|
|
4. Session encryption: Cookie is encrypted (not readable without secret)
|
||
|
|
5. `mix precommit` passes
|
||
|
|
|
||
|
|
## Security claims checklist
|
||
|
|
|
||
|
|
After completing this plan, Berrypod can legitimately claim:
|
||
|
|
|
||
|
|
- [x] No plugin vulnerabilities (architectural)
|
||
|
|
- [x] No filesystem code execution (architectural)
|
||
|
|
- [x] SQL injection prevented (Ecto)
|
||
|
|
- [x] CSRF protection built-in (Phoenix)
|
||
|
|
- [x] Bcrypt password hashing
|
||
|
|
- [x] Timing-attack-safe authentication
|
||
|
|
- [x] 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.
|