# 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.
