Security Audit 2026-09 (A1)
Repo-wide security audit: dependencies, secrets, auth/OTP, webhooks, R2, CSP, Shopify blast radius. 19 findings, 0 P0.
A read-only, repo-wide security audit completed 2026-09-03 — the first security pass this repo has had. Scope: dependency CVEs, secrets hygiene (tree + full git history), better-auth phone OTP / account linking / hard delete, Shopify webhook HMAC, R2 upload authorization and signed URLs, CSP, server-side gates on the backend modules the dashboard audit never covered, the ALLOW_TEST_PHONE_OTP gate, and the shared-live-Shopify-store blast radius. No code or state was changed.
Verdict
Zero P0s. The authorization layer extends cleanly to every module — a full census of mobileLoyalty, cartSync, skinScan, userAddresses, search, webhook internals, gamificationLoyalty/queries, and auth/queries found no public function returning user-scoped data from an argument, and the custom OTP verifyOTP override correctly re-implements Better Auth's attempt tracking (verified against plugin source).
Risk concentrates in operations, not code: credentials in git history with no rotation evidence, one live Shopify store shared by all environments with no write guard on main, suspension that doesn't bind existing sessions, and edge rate limits keyed on attacker-picked inputs.
Tally: 19 findings
| Severity | Count | Headline |
|---|---|---|
| P0 | 0 | — |
| P1 | 2 | Crown-jewel credentials in git history (Firebase service-account key, Shopify Admin tokens) + no secret scanning anywhere; one live Shopify store shared by dev/staging/prod with the write guard (PR #21) still unmerged |
| P2 | 8 | Spoofable XFF on staff sign-in rate limit; unbounded SMS spend; suspended users keep full API access on live sessions; pre-registration account-linking takeover chain; 7-day signed URLs; gateless undeclared R2 uploads; incomplete GDPR erasure; test-phone OTP active on staging |
| P3 | 9 | Fail-open pre-checks, unthrottled sign-up, R2 prefix privacy model, skinScan view audit gap, dev origins in prod config, convex env list prints secrets, localStorage sessions (mitigated by CSP fix), runtime decode-uri-component advisory, build-time advisories |
What was already right
OTP brute-force bound (3 attempts → code dead, 15-min expiry, 10 verifies/hr), rate limits exactly as specified (send 3/hr, verify 10/hr), HMAC timing-safe compare with correct 401-vs-500 retry semantics on all 13 webhook routes, R2 mint-record ownership (including the no-auth-context onSyncMetadata design), fail-closed test-phone gate (prod does not set the flag), hard delete revoking sessions first, and dashboard HTTP exports carrying the active-profile guard.
Full detail: docs/audits/security-2026-09/security-audit-2026-09.md at the repo root (threat model, per-finding evidence, 10 verified-correct controls, 6-entry fixed-in-flight register, fix tasks SX-1…SX-12, owner decisions SO-1…SO-5).