feat(api): harden the app and add deploy + e2e (M7) #72

Merged
TimCane merged 14 commits from feat/m7-hardening into main 2026-07-06 22:07:24 +01:00
TimCane commented 2026-07-06 21:47:02 +01:00 (Migrated from github.com)

Summary

  • per-IP rate limits (create/join/resolve/global) with 429 + Retry-After
  • receipt upload hardening: magic-byte sniff, size cap -> 413, decode-bomb dimension guard
  • security headers (nosniff, no-referrer, same-origin CSP, prod HSTS); CORS stays dev-only
  • no-PII logging + caps enforcement guarded by integration tests
  • two-phone and manual-entry puppeteer e2e specs, run in CI against the full prod compose stack
  • prod compose, multi-stage backend image (SPA -> dotnet publish), Caddy TLS proxy, ephemerality verify script
  • closes #56, #57, #58, #59, #60, #61, #62, #63

Test plan

  • backend integration tests cover rate limits, upload rejection, security headers, no-PII, caps
  • CI e2e job runs both puppeteer specs against the compose stack
  • manual gate: live TLS deploy, two-phone on real phones, one-hour ephemerality check (A4/A5)
## Summary - per-IP rate limits (create/join/resolve/global) with 429 + Retry-After - receipt upload hardening: magic-byte sniff, size cap -> 413, decode-bomb dimension guard - security headers (nosniff, no-referrer, same-origin CSP, prod HSTS); CORS stays dev-only - no-PII logging + caps enforcement guarded by integration tests - two-phone and manual-entry puppeteer e2e specs, run in CI against the full prod compose stack - prod compose, multi-stage backend image (SPA -> dotnet publish), Caddy TLS proxy, ephemerality verify script - closes #56, #57, #58, #59, #60, #61, #62, #63 ## Test plan - backend integration tests cover rate limits, upload rejection, security headers, no-PII, caps - CI e2e job runs both puppeteer specs against the compose stack - [ ] manual gate: live TLS deploy, two-phone on real phones, one-hour ephemerality check (A4/A5)
TimCane commented 2026-07-06 22:05:44 +01:00 (Migrated from github.com)

Code review (medium effort) - fixes applied

Committed to this branch:

  • Rate-limit case bypass (RateLimiting.cs): endpoint matching was ordinal, but ASP.NET routing is case-insensitive - GET /api/v1/Codes/ABCDEF reached the endpoint yet skipped the resolve buckets (10/min + 100/day), and POST /api/v1/Sessions dodged the 5/hour create cap. Now matched case-insensitively.
  • SPA fallback shadowed the API (Program.cs): unmatched /api and /hubs GETs fell through to index.html (200) in prod instead of 404. Added narrower fallbacks.
  • Upload cap drift (Program.cs): the transport body cap read a duplicated 10MB literal; now derived from SessionOptions.MaxUploadBytes so it cannot diverge from the per-image check.
  • Proxy-trust footgun (ForwardedHeadersConfiguration.cs): Enabled=true with no KnownProxies/KnownNetworks trusted nobody and collapsed every client onto the proxy IP (one shared bucket). Now fails fast at startup.
  • JPEG marker walk (ImageDimensions.cs): standalone 2-byte markers (TEM, RSTn, SOI/EOI) advanced the offset by 1, false-rejecting an otherwise valid header. Fixed; docs/04 now records the dimension trigger for the 413.
  • Ephemerality gate false-pass (verify-ephemerality.sh): an unreachable redis/minio read as 0 and the gate printed PASS having verified nothing; dbsize + set -e now abort loudly.
  • Container ran as root (Dockerfile): now runs as the image's non-root user.
  • Earlier: rate limits are resolved through DI so test hosts can loosen them (the original backend CI failure).

Follow-ups (not fixed here)

  • minio-init masks mc ilm rule add failure with || true; exit 0, so the 1-day expiry backstop can silently fail to install - needs an idempotent-but-loud install; skipped to avoid breaking redeploy idempotency untested.
  • Default TRUSTED_PROXY_NETWORK=172.16.0.0/12 trusts the whole Docker bridge; a co-located container (e.g. the OCR sidecar) could spoof X-Forwarded-For. Scope to the proxy's address in real deployments.
  • Per-IP limits key on the full IP and use fixed windows: an IPv6 /64 rotates freely, and a 2x burst is allowed across the window boundary. Matches the docs/10 spec; noted for awareness.
  • CSP connect-src 'self' wss: - the bare wss: is broader than needed (SignalR is same-origin); 'self' should cover it.

Verified: dotnet build clean, script bash -n clean. Integration/e2e are CI-only in this devcontainer.

## Code review (medium effort) - fixes applied Committed to this branch: - **Rate-limit case bypass** (`RateLimiting.cs`): endpoint matching was ordinal, but ASP.NET routing is case-insensitive - `GET /api/v1/Codes/ABCDEF` reached the endpoint yet skipped the resolve buckets (10/min + 100/day), and `POST /api/v1/Sessions` dodged the 5/hour create cap. Now matched case-insensitively. - **SPA fallback shadowed the API** (`Program.cs`): unmatched `/api` and `/hubs` GETs fell through to `index.html` (200) in prod instead of 404. Added narrower fallbacks. - **Upload cap drift** (`Program.cs`): the transport body cap read a duplicated `10MB` literal; now derived from `SessionOptions.MaxUploadBytes` so it cannot diverge from the per-image check. - **Proxy-trust footgun** (`ForwardedHeadersConfiguration.cs`): `Enabled=true` with no `KnownProxies`/`KnownNetworks` trusted nobody and collapsed every client onto the proxy IP (one shared bucket). Now fails fast at startup. - **JPEG marker walk** (`ImageDimensions.cs`): standalone 2-byte markers (TEM, RSTn, SOI/EOI) advanced the offset by 1, false-rejecting an otherwise valid header. Fixed; `docs/04` now records the dimension trigger for the 413. - **Ephemerality gate false-pass** (`verify-ephemerality.sh`): an unreachable redis/minio read as `0` and the gate printed PASS having verified nothing; `dbsize` + `set -e` now abort loudly. - **Container ran as root** (`Dockerfile`): now runs as the image's non-root user. - Earlier: rate limits are resolved through DI so test hosts can loosen them (the original backend CI failure). ## Follow-ups (not fixed here) - `minio-init` masks `mc ilm rule add` failure with `|| true; exit 0`, so the 1-day expiry backstop can silently fail to install - needs an idempotent-but-loud install; skipped to avoid breaking redeploy idempotency untested. - Default `TRUSTED_PROXY_NETWORK=172.16.0.0/12` trusts the whole Docker bridge; a co-located container (e.g. the OCR sidecar) could spoof `X-Forwarded-For`. Scope to the proxy's address in real deployments. - Per-IP limits key on the full IP and use fixed windows: an IPv6 /64 rotates freely, and a 2x burst is allowed across the window boundary. Matches the docs/10 spec; noted for awareness. - CSP `connect-src 'self' wss:` - the bare `wss:` is broader than needed (SignalR is same-origin); `'self'` should cover it. Verified: `dotnet build` clean, script `bash -n` clean. Integration/e2e are CI-only in this devcontainer.
Sign in to join this conversation.
No description provided.