ProjectR Docs
Audit 2026-09

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

SeverityCountHeadline
P00—
P12Crown-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
P28Spoofable 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
P39Fail-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).

On this page