# Security Audit — 2026-08-20 (first-party, focused) Scope: the attack surface added since the v0.10.0 generic audit — interactive SSO (Auth0 OIDC, ADR-0062), the Stripe billing webhook loop (ADR-0053), the public namespace, distributed rate limiting, and the release-engineering gate. Third-party audit remains a budget-dependent follow-up for the public launch. ## Summary | # | Severity | Area | Finding | Status | |---|----------|------|---------|--------| | 1 | High | Dependencies | nanoid < 3.3.18 (GHSA-2v37-7h3g-55p8) via vitest→vite→postcss | Fixed (3.3.18, audits clean) | | 2 | Low | SSO | `jwtVerify` without explicit algorithm pinning | Fixed (`algorithms: ['RS256']`) | | 3 | Low | SSO | duplicated `parseCookies` threw URIError on malformed cookie → 500 | Fixed (shared safe parser) | No critical findings. No real secrets in git history (only `sk_live_...` / `whsec_123` placeholders in env examples and test fixtures). ## Findings ### 1. High — nanoid 3.3.17 (GHSA-2v37-7h3g-55p8) `custom generators can loop indefinitely when size is zero`. Reachable only through dev tooling (`vitest` → `vite` → `postcss`), not the production runtime, but the root audit was no longer clean as claimed in the v0.10.0 roadmap. Fixed with `npm audit fix` → nanoid 3.3.18. Root and frontend both report 0 vulnerabilities (`--audit-level=moderate`). ### 2. Low — algorithm pinning in Auth0Provider `src/providers/auth/auth0-provider.ts` verified JWTs against the remote JWKS without an explicit `algorithms` list. jose derives the allowed algorithms from the key type, which already blocks symmetric-key confusion, but an explicit `algorithms: ['RS256']` is cheap defense in depth and makes the accepted signing family self-documenting. Fixed. ### 3. Low — cookie parser robustness (SSO + auth middleware) Two identical `parseCookies` copies existed in `src/api/routes/oidc.ts` and `src/api/middleware/auth.ts`. Both applied `decodeURIComponent` unconditionally: a `Cookie: dominus_oidc=%` header (thrown `URIError`) surfaced as a 500 on unauthenticated endpoints (`/oidc/callback`, `/oidc/me`). Consolidated into one safe parser (`src/utils/cookies.ts`) that skips malformed percent-encoding. No behavior change for well-formed cookies. ## Reviewed — no change required - **Login CSRF on `/oidc/start`** — no risk. The IdP authenticates the user with their own credentials; the resulting session is bound to the HMAC-signed state cookie and PKCE verifier, so an attacker cannot inject or hijack a session. `/api/v1/auth/oidc/*` sits behind the per-IP auth rate limiter (30 req/min, index.ts). - **PKCE/state flow** — S256 challenge, server-side code+verifier exchange with the client secret, state HMAC verified with `timingSafeEqual`, 10-minute transient-cookie TTL. Sound. - **Session JWT** — HS256 pinned on verification, HKDF-derived key from `AUTH0_CLIENT_SECRET` (rotation = global logout, documented), bounded TTL. Logout does not invalidate the JWT server-side (stateless design, documented deferral in ADR-0062); acceptable at this scale. - **Stripe webhook** — raw body preserved (`express.raw` before `express.json`), signature verified via `constructEvent`, DB-backed idempotency claim fail-closed (duplicate transitions rejected), unknown price IDs never overwrite the plan (`COALESCE` guard), `past_due` on `invoice.payment_failed`, cancel on `subscription.deleted`. Solid. - **Checkout metadata plan** — trusted from session metadata set server-side at session creation; a misconfigured price ID could grant a plan the customer did not pay for. Operator-config error only, not exploitable by clients; noted for the custom-pricing work. - **SSRF on the public namespace** — `/public/domain/:domain` validates via `isValidDomain` before any provider call; RDAP/DNS/DoH/DoT/wayback fetch fixed base URLs with the validated domain as path/query only. No user-controlled URL reaches `fetch`. - **Distributed rate limiting** — Redis fixed-window store with per-instance MemoryStore fallback (degraded, never disabled); per-domain limiter on the public router; `TRUST_PROXY_DEPTH` defaults to 0 (community-correct) and is set to 1 in the cloud configmap behind Caddy. Correct as configured. - **Security headers** — strict CSP (`script-src 'self'`), HSTS gated on HTTPS, `frame-ancestors 'none'`, permissions policy; COEP deliberately omitted (documented). Adequate. - **RLS** — `ENABLE` (0030) + `FORCE` (0047) with `runWithTenant` binding; PG-only paths tested. Adequate. - **Git history secrets scan** — `sk_live_|AKIA|ghp_|whsec_|AIza|private keys` pickaxe over all refs: 2 hits, both placeholders in `terraform.tfvars.example` and test fixtures. ## Actions - [x] `npm audit fix` (nanoid 3.3.18) - [x] pin `algorithms: ['RS256']` in `Auth0Provider.validate` - [x] consolidate + harden `parseCookies` in `src/utils/cookies.ts` ## Follow-up (out of scope) - Third-party audit before the public launch (budget-dependent). - Custom-pricing work must cross-check the paid price against the granted plan instead of trusting checkout metadata alone. - IdP-side logout and refresh-token rotation (ADR-0062 deferrals).