Ctrl K
ComunidadGuías…

docs/security/audit-2026-08.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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).