# Security Audit Marathon — Design

**Date:** 2026-06-10
**Status:** APPROVED (design)
**Owner:** orchestrator (Opus) + cursor-agent labor
**Goal:** answer "is the system vulnerable?" with evidence, then drive every confirmed
vulnerability to fixed-and-verified via scan-fix-rescan iterations.

## 1. Scope & Threat Model

Multi-tenant business-management SaaS (invoicing, payments, CRM, contracts, portals).
Assets at risk, ranked:

1. **Money paths** — Morning (greeninvoice) payment adapter, invoices, billing, payouts.
2. **Cross-tenant data** — every table keyed by `tenant_id`; IDOR = breach.
3. **Account takeover** — sessions, refresh tokens, 2FA, password reset, admin plane.
4. **PII** — customers, contractors, bank statements, GDPR exports, Israeli tax data
   (bituach leumi, nii advances).
5. **Platform integrity** — admin impersonation, public API keys, OAuth, integrations
   (telegram, zapier, make), AI assistant dispatch.

Attack surface (all in scope):

| Surface | Code |
|---------|------|
| Main API Worker | `apps/zync-api/src` (~80 route modules, middleware, cron, queues, DOs) |
| Public API Worker | `apps/zync-public-api`, `packages/public-api` |
| App SPA | `apps/zync-app` |
| Admin SPA | `apps/zync-admin` |
| Marketing site | `apps/zync-www` |
| Shared packages | `packages/{auth,db,payments,integrations,storage,realtime,...}` |
| Infra config | `wrangler.toml` per app, CF KV/R2/DO/queues bindings, Neon PG |

**Out of scope:** DoS/load testing, external network pentest, social engineering,
physical. Runtime-only races are flagged by reasoning (not load-proven) and marked
`confidence: needs-runtime-proof`.

## 2. Architecture: scan-fix-rescan pipeline

```
Iteration N:
  SCAN   — 11 parallel cursor-agent scanners (1 vuln class per prompt)
           → findings JSON per class
  TRIAGE — orchestrator + advisor: confirm/reject, severity, code-wrong vs spec-wrong,
           dedupe vs known findings, dependency-order fix waves
  FIX    — cursor-agent fixers in worktrees (1 finding-cluster per prompt, parallel)
           → per-wave code gate (Opus code-review on diff) + process gate (advisor)
  RESCAN — fresh cursor-agent per class with findings list: verify each fix, hunt
           regressions + new instances of same class
Repeat until convergence (§7). Max 3 iterations; residual risk documented.
```

Labor split (per marathon labor model): cursor-agent does ALL scanning and fixing;
Opus orchestrates, triages, runs live probes, gates. Claude writes zero implementation
code (IRON LAW).

## 3. Scan matrix — 11 vulnerability classes

Each class = one scanner prompt (parallel dispatch). Scanner reads code; reports
findings JSON only; NEVER edits code or specs.

| ID | Class | Focus areas |
|----|-------|-------------|
| S1 | AuthN & sessions | login/refresh rotation/reset/verify-email/2FA bypass, JWT (alg, expiry, secret use), cookie flags, blocklist, session fixation, user-version revocation gaps |
| S2 | AuthZ & RBAC | per-route permission checks vs route inventory, admin plane (`routes/admin`, `admin-auth.ts`), impersonation guard (`block-impersonation-ops.ts`), field-permissions, module gates |
| S3 | Tenant isolation / IDOR | every query in `packages/db/src/queries` + route param→query flows; portals (customer/contractor/portal routes), shared links, signed pages (`sign.ts`, `proposals-public.ts`, `contract-signing`) |
| S4 | Injection — SQL/KV | raw interpolation in SQL, `sql.unsafe`, dynamic ORDER BY/LIMIT, KV key construction from user input, search query building |
| S5 | XSS & output encoding | `dangerouslySetInnerHTML`/`innerHTML` sites, tiptap rendering, email templates, notification render, PDF/report generators, CSV/Excel formula injection in exporters |
| S6 | SSRF & outbound | user-controlled URLs in integrations/webhooks-out/custom SMTP/make/zapier, redirect validation, internal metadata endpoints reachability from Workers |
| S7 | Inbound webhooks, cron, queues, DO | Morning verifyWebhook (re-fetch-confirm), telegram auth, `routes/cron` + `cron/` auth, queue message trust, DO access control, replay protection |
| S8 | Files & storage | R2 upload validation (type/size/path), signed URL scoping + expiry, `portal-files`, `attachments`, `unified-attachments`, public R2 invoice snapshots, path traversal, content-type sniffing/XSS via upload |
| S9 | Payments & money logic | amount/currency tampering, idempotency, webhook replay→double-credit, status-transition races (CG2-a/CG2-b context), refund/credit paths, bulk invoice generation |
| S10 | Public API & OAuth | API key generation/storage/verify (hashing, timing), scope enforcement (`oauth-scope-map.ts`, `scopes.ts`), rate limits, tenant binding of keys, enumeration, pagination leaks |
| S11 | Secrets, config, headers, deps | secret-pattern grep across repo+history (gitleaks-style), wrangler `[vars]`, CSP/CORS/security headers on all 5 apps, cookie domain notes, `pnpm audit`, logging of secrets/PII, source-map exposure |

Cross-class catch-all: each scanner ends with "anything severe outside your class →
report under `class: other`".

## 4. Findings schema & storage

`docs/plans/audit/security/<class-id>.json` (extends existing audit JSON convention):

```json
{
  "class": "S3",
  "iteration": 1,
  "findings": [{
    "id": "S3-001",
    "severity": "P0|P1|P2|P3",
    "title": "",
    "code_ref": "file:line",
    "evidence": "concrete code/flow, not speculation",
    "repro": "curl/steps where applicable",
    "suggested_fix": "",
    "confidence": "confirmed|likely|needs-runtime-proof",
    "triage": null
  }]
}
```

Severity rubric:
- **P0** — exploitable now: cross-tenant read/write, money manipulation, account
  takeover, unauthed admin/cron/queue access, secret exposure.
- **P1** — serious weakness needing one extra condition (missing rate limit on auth,
  token-in-URL, weak verify, missing webhook replay guard).
- **P2** — defense-in-depth gap (missing tenant predicate behind a checked caller,
  missing header, verbose errors).
- **P3** — hardening/informational.

Triage values (set by orchestrator+advisor only, never scanner): `confirmed-code-fix`,
`confirmed-spec-fix` (spec wrong/missing — per spec-source-of-truth law), `rejected`
(+reason), `accepted-risk` (+reason, user-visible in final report), `duplicate-of:<id>`.

**Spec incorporation (user directive 2026-06-10):** every triaged finding gets an
`owning_spec` field — the existing `docs/specs/*.md` file governing that behavior.
On fix, the fixer updates that EXISTING spec in the same commit whenever the secure
behavior is absent from or contradicted by it (orchestrator authors the exact spec
edit text). Security requirements discovered by this audit live in the owning feature
specs — this design doc is process-only and carries no normative security requirements
of its own. Code, fix, and existing spec must never disagree.

Seed findings (enter triage as iteration-1 input, no rescan needed to discover them):
- `docs/plans/audit/session-security.json` — open P0/P1 (admin-sessions auth mismatch,
  sessionGuard unwired, recordSessionOnLogin uncalled, permissions unseeded).
- `docs/plans/audit/_class-tenant-isolation-classified.json` — DiD holes.
- Security-class findings in other open audit JSONs (orchestrator sweeps them at triage).

## 5. Triage protocol

1. Orchestrator reads all class JSONs, dedupes, validates evidence (opens cited files —
   core law: verify scanner claims).
2. Severity calibration against rubric; downgrades/upgrades recorded with reason.
3. Code-wrong vs spec-wrong decided by orchestrator + advisor (scanners/fixers NEVER
   edit specs). Genuine product-intent calls → escalate to user; everything else
   decided now (standing order: no deferral).
4. Fix waves built dependency-ordered: P0 first, then P1, then P2 batched by area;
   P3 single hardening wave at end. Findings touching same files cluster into one
   fixer prompt; otherwise parallel.

## 6. Fix waves & gates

Per cursor-orchestrator skill, unchanged:
- Fixers run in worktrees; 1 finding-cluster per prompt; commit per cluster.
- Fix prompt includes: finding JSON, triage verdict, suggested fix as hint not mandate,
  "update matching spec section" ONLY when triage = `confirmed-spec-fix` with exact
  spec edit text provided by orchestrator.
- **Code gate:** Opus `feature-dev:code-reviewer` over the wave diff — security
  regression focus.
- **Process gate:** `advisor()` between waves.
- Both gates feed cursor-agent fixer loop until clean. Per-wave verify floors (1-7:
  typecheck, build, lint, tests, e2e where touched) before advancing.
- Live probes by orchestrator where cheap: curl dev deployment for header/CORS/auth
  checks (`dev.zync.is`, `app.dev.zync.is`).

## 7. Convergence criterion

Audit is DONE when, in one full iteration:
1. Every prior P0/P1/P2 finding verified `fixed` by a fresh rescan agent (evidence:
   re-read code path), and
2. Rescan produces **zero new P0/P1** findings (new P2/P3 allowed → fixed in a final
   wave without full re-iteration), and
3. All gates clean.

Hard stop after 3 full iterations: remaining findings documented in final report as
residual risk with severity + recommendation.

## 8. Final deliverable

`docs/plans/audit/security/REPORT.md`: verdict ("is the system vulnerable?" — answered
per severity tier), findings table (found/fixed/accepted-risk/residual), iteration
history, live-probe results, spec updates made, recommendations (tooling/CI: semgrep +
gitleaks adoption noted as follow-up, not in-scope work).

## 9. Error handling

- Scanner timeout (30min) → re-dispatch with narrowed area split (proven marathon rule).
- Fixer timeout (58min) → split cluster, re-dispatch.
- Scanner returns prose instead of JSON → re-prompt with schema, 1 retry, then
  orchestrator extracts findings manually into JSON.
- Contradictory findings between classes → triage resolves; both scanners' evidence kept.
- Fix breaks tests/build → fixer loop owns repair; gate blocks wave until green.

## 10. Testing

The audit's own QA = rescan by fresh agents + two gates + live probes. Fixes must keep
existing test suites green (floors). New regression tests required for every P0 fix
(fixer writes them; code gate enforces).

## Architecture Decisions

- **No new runtime modules** — this is an orchestration-process spec; Phase-2 depth
  analysis N/A (size gate: zero proposed code modules).
- **Class-based decomposition accepted** over spec-slug decomposition: security vulns
  cluster by class, not by feature spec; avoids 177-slug redundancy.
- **SAST tooling collapsed into S11** rather than separate phase: semgrep/gitleaks not
  installed; generic rules weak on Hono/Workers authz logic; cheap wins (pnpm audit,
  secret grep, header review) fit one scanner unit.
- **Rejected: single mega-scanner prompt** — measured marathon result: 1 unit per
  prompt parallel ≫ batched; mega-prompt loses depth and times out.
