# Security Audit Marathon Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: executed by the orchestrator via
> `cursor-orchestrator` (NOT /ship — no Claude-written code exists in this plan;
> all labor is cursor-agent dispatch). Steps use checkbox (`- [ ]`) syntax.

**Goal:** Answer "is the system vulnerable?" with evidence and drive every confirmed P0–P2 vulnerability to fixed-and-verified via scan-fix-rescan iterations.

**Architecture:** 11 parallel cursor-agent scanners (one vulnerability class each) emit findings JSON → orchestrator+gates triage (code-wrong vs spec-wrong) → cursor-agent fixers in worktrees with per-wave code/process gates → fresh-agent rescan until convergence (max 3 iterations). Spec: `docs/specs/2026-06-10-security-audit-marathon-design.md`.

**Tech Stack:** cursor-agent via `~/.claude/skills/cursor-orchestrator/ca.sh`, git worktrees, findings JSON in `docs/plans/audit/security/`, Opus `feature-dev:code-reviewer` gates, live curl probes against `app.dev.zync.is`.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1 (setup) | `docs/plans/audit/security/_scanner-preamble.md`, scan worktree | single task |
| 2 | Tasks 2–12 (scanners S1–S11) | `docs/plans/audit/security/S{1..11}.json` (distinct files, read-only code access) | ✅ no overlap |
| 3 | Task 13 (collect + commit) | copies JSONs to main repo | single task |
| 4 | Task 14 (triage + live probes) | triage fields in JSONs, `FIXPLAN.md` | single task |
| 5 | Task 15 (fix waves — DYNAMIC) | per-cluster worktrees, code + specs | clusters parallel when file-disjoint |
| 6 | Task 16 (rescan S1–S11) | `iteration: N+1` JSONs | ✅ no overlap |
| 7 | Task 17 (convergence check) | — | single task |
| 8 | Task 18 (final report) | `docs/plans/audit/security/REPORT.md` | single task |

Waves 5–7 loop until convergence (§7 of spec), max 3 iterations.

---

### Task 1: Setup — findings dir, scanner preamble, scan worktree

**Wave:** 1 / **Blocks:** Tasks 2–12 / **Blocked by:** —

**Files:**
- Create: `docs/plans/audit/security/_scanner-preamble.md`
- Create: git worktree `.claude/worktrees/sec-scan-i1` from `master`

- [ ] **Step 1: Create dirs + worktree**

```bash
cd /home/user/Projects/zync.is
mkdir -p docs/plans/audit/security
git worktree add .claude/worktrees/sec-scan-i1 master
mkdir -p .claude/worktrees/sec-scan-i1/docs/plans/audit/security
```

- [ ] **Step 2: Write scanner preamble** (orchestrator writes this file — process doc, not implementation code; IRON LAW intact)

Content of `docs/plans/audit/security/_scanner-preamble.md` (also copy into the worktree at the same relative path):

```markdown
# Security Scanner Rules (read fully before scanning)

You are a security auditor. READ-ONLY on all code. You MUST NOT edit any source
file, spec, or config. You MUST NOT run git commit/add/push. Your ONLY write is
your single output JSON file.

Output: docs/plans/audit/security/<YOUR-CLASS-ID>.json with EXACTLY this shape:
{
  "class": "<YOUR-CLASS-ID>",
  "iteration": 1,
  "findings": [{
    "id": "<CLASS>-NNN",
    "severity": "P0|P1|P2|P3",
    "title": "one line",
    "code_ref": "path/to/file.ts:line",
    "evidence": "concrete code/flow you READ — quote the lines. No speculation.",
    "repro": "curl/steps if applicable, else empty string",
    "suggested_fix": "one paragraph",
    "confidence": "confirmed|likely|needs-runtime-proof",
    "triage": null
  }]
}

Severity rubric:
- P0: exploitable now — cross-tenant read/write, money manipulation, account
  takeover, unauthenticated admin/cron/queue/DO access, secret exposure.
- P1: serious weakness one condition away (missing rate limit on auth endpoint,
  token in URL, weak/missing verification, missing replay guard).
- P2: defense-in-depth gap (missing tenant predicate behind checked caller,
  missing security header, verbose error detail).
- P3: hardening/informational.

Rules:
1. Evidence = lines you actually read. Cite exact file:line. A finding without
   readable evidence is worthless — drop it.
2. Trace data flow end-to-end before claiming exploitability (route → middleware
   → query). Check middleware chains in apps/zync-api/src/index.ts and route
   files before claiming an endpoint is unauthenticated.
3. Severity per rubric above, not gut feeling.
4. You do NOT decide whether a spec is wrong. Never edit specs. If implementation
   matches code-intent but contradicts docs/specs/*.md, report it with
   confidence "likely" and note the spec file in evidence.
5. Anything severe you notice OUTSIDE your class: include it with id prefix
   "OTHER-" so the orchestrator can route it.
6. Exclude: .claude/worktrees/**, node_modules, dist, screenshots, tmp — scan
   apps/*/src, packages/*/src, root configs, wrangler.toml files only.
7. If output would exceed ~40 findings, keep the 40 most severe; note truncation
   in a final finding titled "TRUNCATED".
```

- [ ] **Step 3: Commit preamble in main repo**

```bash
cd /home/user/Projects/zync.is && git add docs/plans/audit/security/_scanner-preamble.md && git commit -m "audit(security): scanner preamble"
```

---

### Tasks 2–12: Scanner dispatches S1–S11

**Wave:** 2 / **Blocks:** Task 13 / **Blocked by:** Task 1

All 11 dispatched in parallel. Each via:

```
mcp__plugin_context-mode_context-mode__ctx_execute(language: "bash", code:
  `~/.claude/skills/cursor-orchestrator/ca.sh --workspace /home/user/Projects/zync.is/.claude/worktrees/sec-scan-i1 --trust "<PROMPT>" --task-slug "sec-scan-<CLASS>"`)
```

Timeout per marathon model: 30min; on timeout split the class's focus list in two and re-dispatch halves.

Each `<PROMPT>` = the line `First read docs/plans/audit/security/_scanner-preamble.md and follow it exactly. Your class: <ID>. Write docs/plans/audit/security/<ID>.json.` followed by the class focus below.

- [ ] **Task 2 — S1 AuthN & sessions:**
  "Audit authentication: apps/zync-api/src/routes/auth/**, routes/admin/auth/**, src/lib/cookies.ts, src/lib/issue-session.ts, src/middleware/auth.ts, src/middleware/admin-auth.ts, src/middleware/session-guard.ts, src/middleware/user-version.ts, packages/auth/src/**, src/routes/reauth.ts, src/routes/oauth/**. Hunt: login/refresh-rotation flaws (reuse-after-rotate, race), password-reset token entropy/expiry/single-use, email-verify bypass, 2FA bypass paths (issueSessionForTenantWith2FA call sites — can a session skip the 2FA step?), JWT verification (alg confusion, missing expiry check, secret reuse between user/admin planes), cookie attributes, session fixation, logout/blocklist completeness (does refresh survive logout?), user-version revocation gaps, timing-unsafe comparisons (token equality via ===), rate limiting on login/reset/2FA endpoints."

- [ ] **Task 3 — S2 AuthZ & RBAC:**
  "Audit authorization: enumerate EVERY route registration in apps/zync-api/src/index.ts and all files in src/routes/** (run: grep -rn \"\\.(get\\|post\\|put\\|patch\\|delete)(\" src/routes | wc -l first to size the inventory; cover ALL of it, not a sample). For each route verify: auth middleware applied, permission/role check present and correct for the operation, module gate (require-module-enabled) where spec'd. Deep-check: routes/admin/** vs admin-auth, impersonation (impersonate.ts, impersonation-end.ts, block-impersonation-ops.ts — what ops are NOT blocked during impersonation that should be?), field-permissions enforcement, privilege escalation via roles.ts / users routes (can MEMBER grant self OWNER?), mass-assignment in update handlers (body fields passed to DB unfiltered)."

- [ ] **Task 4 — S3 Tenant isolation / IDOR:**
  "Audit cross-tenant isolation. Read docs/plans/audit/_class-tenant-isolation-classified.json first (known DiD holes — do NOT re-report those ids; verify 3 randomly to confirm classification still holds, report if stale). Then hunt NEW holes: every query function in packages/db/src/queries/** taking an entity id — does it constrain tenant_id, and does EVERY caller pass session.tid (not a body/param tenant id)? Trace: route param → handler → query. Priority surfaces: portal routes (src/routes/portal/**, portal-files.ts, portal-kb.ts, customer/contractor portals), public signing pages (sign.ts, proposals-public.ts, contracts/**, catalog-public.ts), attachments/unified-attachments/files routes, search.ts (does index leak cross-tenant?), realtime.ts + durable-objects/TenantRealtimeDO.ts (room access control), entity-history.ts, audit-log.ts, exports. Also: tenant id from request body/query trusted anywhere instead of session.tid?"

- [ ] **Task 5 — S4 Injection SQL/KV:**
  "Audit injection: all SQL in packages/db/src/** and any inline SQL in apps/zync-api/src/**. Hunt: string interpolation/concat into SQL (template literals embedding variables inside query text rather than parameters), sql.unsafe/raw usage, dynamic ORDER BY / sort column / LIMIT from user input, LIKE patterns unescaped, JSON path injection, search query construction (src/search/**, routes/search.ts), CSV import parsing → SQL (routes/imports/**, bank-statement-import), KV key construction from user input (could user-controlled string collide with blocklist:/quota:/other key namespaces?), Durable Object name derivation from user input."

- [ ] **Task 6 — S5 XSS & output encoding:**
  "Audit XSS: grep dangerouslySetInnerHTML and innerHTML across apps/zync-app/src, apps/zync-admin/src, apps/zync-www/src, packages/ui/src — for each site, trace the value source; flag any reachable from user/tenant data without sanitization (which sanitizer? DOMPurify config?). Tiptap/rich-text render paths (packages/public-api tiptap.ts, proposals-editor.ts, kb rendering, contract content). Email templates (email-templates.ts, packages/notifications — user data interpolated into HTML emails?). notifications.render.ts. PDF/report generators (src/lib/report-generators/**, src/reports/** — HTML-to-PDF with user content?). CSV/Excel exporters (packages/exporters — formula injection: cell starting with = + - @). markdown rendering anywhere. URL attributes (href={userValue} allowing javascript:)."

- [ ] **Task 7 — S6 SSRF & outbound:**
  "Audit SSRF/outbound: grep fetch( across apps/zync-api/src and packages/{integrations,notifications,payments,make,zapier}/src. For each outbound call: is the URL (or any part — host, path, port) user/tenant-controlled? Hunt: tenant webhook endpoints (features/webhooks/**, lib/time-webhooks.ts — can a tenant point a webhook at internal/metadata addresses; is there an allow/deny on private IPs?), custom SMTP settings (custom-smtp spec), integration configs (services/integrations/**, features/settings/integrations/**), make/zapier callback URLs, telegram API calls, AI dispatch (src/ai/dispatch-to-assistant.ts — prompt injection routing user text into tool-capable agents?), redirect/return-url validation on oauth + payment return flows (open redirect), webhook URL re-validation at delivery time vs save time."

- [ ] **Task 8 — S7 Inbound webhooks, cron, queues, DO:**
  "Audit inbound machine surfaces: src/routes/webhooks/** (every inbound webhook — what authenticates it? Morning/greeninvoice handler must use re-fetch-confirm per docs/specs payment spec, NOT trust payload), telegram webhook (routes/telegram/** — secret token check?), src/routes/cron/** + src/cron/** (what stops a public caller from invoking cron HTTP routes? CF cron triggers vs exposed routes), src/queue/** + src/queues/** (message provenance — can any user-facing endpoint enqueue arbitrary messages that queue handlers trust?), durable-objects/** (who can reach DO endpoints?), intake/email-expense.ts (sender spoofing — what validates the inbound email source?), replay protection on all webhooks (idempotency key / timestamp window), src/routes/public/** and catalog-public.ts (intended-public inventory — anything there that should NOT be public?)."

- [ ] **Task 9 — S8 Files & storage:**
  "Audit file handling: routes/files/**, attachments.ts, unified-attachments.ts, portal-files.ts, packages/storage/src/**. Hunt: upload validation (content-type allowlist or blocklist? size cap? who sets the R2 object key — user-controlled filename → path traversal/overwrite?), download authorization (tenant + entity check before stream? IDOR via attachment id?), signed URL generation (expiry, scope — can a signed URL for file A be replayed for file B?), public R2 invoice-snapshot domain (wrangler.toml invoices-core — what's enumerable? sequential keys?), content-type on serve (user-uploaded HTML/SVG served inline → stored XSS; Content-Disposition?), zip handling in imports, GDPR export files (data/** routes — who can fetch whose export?)."

- [ ] **Task 10 — S9 Payments & money:**
  "Audit money paths: packages/payments/src/**, src/integrations/payment-gateways/**, src/integrations/invoice-adapters/**, routes/billing/**, routes/invoices/**, receipts/**, bad-debt-reclaims.ts, nii-advances.ts, routes/bulk/** (bulk invoice generation), admin-subscription.ts, zync-subscription.ts. Hunt: amount/currency from client trusted anywhere money moves (price computed client-side?), payment webhook → invoice.paid transition (replay → double credit? idempotency? status-transition guard — read docs/plans/audit/wave*-morning-*.md for known CG2-a/CG2-b race context, do not re-report what those gates already track, but verify guards actually in code today), refund/credit authorization, invoice number/snapshot tampering, discount/tax computed server-side?, contractor payouts authorization chain, recurring-invoice mutation by non-owner."

- [ ] **Task 11 — S10 Public API & OAuth:**
  "Audit public API: apps/zync-public-api/src/** (middleware/auth.ts especially), packages/public-api/src/** (scopes.ts, oauth-scope-map.ts, pagination.ts, serializers.ts), src/routes/oauth/**, api-keys-usage.ts, packages/api-usage/src/**. Hunt: API key lifecycle (generation entropy, storage — hashed with what? verify — timing safe?), key→tenant binding (can a key reach another tenant's data through ANY route?), scope enforcement per route (map every public route to its required scope; any route missing a scope check?), rate limiting (per key? bypassable via header spoof?), pagination cursor leaking cross-tenant data or internal ids, serializers over-exposing fields (PII not in OpenAPI contract), OAuth flows (state param CSRF, PKCE, redirect_uri validation, code reuse), error responses enabling key/tenant enumeration."

- [ ] **Task 12 — S11 Secrets, config, headers, deps:**
  "Audit secrets/config/deps: (a) secret-pattern grep across tracked files AND git log -p --since=2026-01-01 for: api keys, tokens, private keys, connection strings (pattern: AKIA, sk-, ghp_, eyJ[A-Za-z0-9], postgres://, -----BEGIN); confirm secrets/ and Docs/ are gitignored AND nothing slipped into history; (b) every wrangler.toml [vars] — anything secret-shaped that belongs in secret bindings?; (c) security headers: how do the 5 apps set CSP, X-Frame-Options/frame-ancestors, HSTS, X-Content-Type-Options, Referrer-Policy — read wrangler.toml [[headers]] blocks + any middleware; flag missing CSP on app/admin (session cookie + XSS = takeover); (d) CORS: src/middleware/cors.ts — credentials+origin combos, null origin, workers.dev origins in prod allow-list; (e) run pnpm audit --json in the worktree root and summarize HIGH/CRITICAL with affected import paths; (f) logging: grep console.log/error in apps/zync-api/src for token/secret/password/PII leakage into logs; (g) source maps / debug endpoints exposed in prod builds."

For each of Tasks 2–12: dispatch → on completion verify the JSON file exists in the worktree and parses (`python3 -c "import json;json.load(open('docs/plans/audit/security/<ID>.json'))"`); prose-instead-of-JSON → one re-prompt with schema, then orchestrator extracts manually.

---

### Task 13: Collect findings, commit iteration-1 scan results

**Wave:** 3 / **Blocks:** Task 14 / **Blocked by:** Tasks 2–12

- [ ] **Step 1:** Copy `S*.json` from scan worktree to main repo `docs/plans/audit/security/`; validate all 11 parse; route any `OTHER-` findings to the right class file.
- [ ] **Step 2:** Commit: `git add docs/plans/audit/security/ && git commit -m "audit(security): iteration-1 scan findings"`

---

### Task 14: Triage + live probes → FIXPLAN

**Wave:** 4 / **Blocks:** Task 15 / **Blocked by:** Task 13

Orchestrator work (no cursor-agent):

- [ ] **Step 1: Verify evidence.** For EVERY P0/P1: open the cited file at cited line; confirm the claim (core law). For P2/P3: spot-check ≥30%. Mark fabricated/stale findings `triage: "rejected"` + reason.
- [ ] **Step 2: Merge seed findings.** Import open security-relevant items from `docs/plans/audit/session-security.json` (P0 admin-sessions auth model, sessionGuard unwired, recordSessionOnLogin uncalled, permission keys unseeded, keepalive absent) and `_class-tenant-isolation-classified.json` (DiD predicates) into the matching class JSONs with `id: "SEED-*"`. Dedupe scanner findings against them (`duplicate-of:`).
- [ ] **Step 3: Live probes** (evidence for header/CORS/auth findings):

```bash
curl -sI https://app.dev.zync.is | grep -i -e strict-transport -e content-security -e x-frame -e x-content
curl -sI https://dev.zync.is | grep -i -e strict-transport -e content-security -e x-frame -e x-content
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://app.dev.zync.is/api/auth/login -H 'Origin: https://evil.example' -H 'Content-Type: application/json' -d '{}'
curl -s -o /dev/null -w '%{http_code}\n' https://app.dev.zync.is/api/cron/run 2>/dev/null
```

  Record outputs in FIXPLAN. (Probes are read-only/unauthenticated against OUR dev deployment.)
- [ ] **Step 4: Severity calibration** against spec §4 rubric; record up/downgrades with reason in the JSON (`triage_note`).
- [ ] **Step 5: Code-wrong vs spec-wrong** per finding (orchestrator + process gate; product-intent questions → surface to user in final report, choose the secure default meanwhile — standing order: no deferral). Set `triage: confirmed-code-fix | confirmed-spec-fix | rejected | accepted-risk | duplicate-of:<id>`.
- [ ] **Step 6: Write `docs/plans/audit/security/FIXPLAN.md`:** clusters (findings grouped by touched-file overlap), wave order P0→P1→P2 (P3 = one final hardening cluster), per-cluster file list proving disjointness of same-wave clusters, exact spec edits for every `confirmed-spec-fix` (orchestrator-authored text the fixer applies verbatim).
- [ ] **Step 6b (user directive): `owning_spec` per finding.** Map every confirmed finding to the existing `docs/specs/*.md` governing it. If the secure behavior is missing from / contradicted by that spec, the cluster's fix prompt includes an orchestrator-authored edit to that EXISTING spec applied in the same commit as the code fix. The marathon design doc carries no normative requirements — existing specs are the single source of truth.
- [ ] **Step 7:** Run process gate (code-review subagent over FIXPLAN as plan-review; advisor unavailable this session). Fix plan per feedback. Commit FIXPLAN + triaged JSONs.

---

### Task 15: Fix waves (DYNAMIC — repeat per cluster wave)

**Wave:** 5 / **Blocks:** Task 16 / **Blocked by:** Task 14

For each fix wave in FIXPLAN order; clusters within a wave dispatched in parallel, each in its own worktree:

- [ ] **Step 1:** `git worktree add .claude/worktrees/sec-fix-<cluster> master`
- [ ] **Step 2:** Dispatch fixer:

```
~/.claude/skills/cursor-orchestrator/ca.sh --workspace /home/user/Projects/zync.is/.claude/worktrees/sec-fix-<cluster> --trust "<FIX PROMPT>" --task-slug "sec-fix-<cluster>"
```

  Fix prompt template (complete, fill bracketed values from FIXPLAN): "Fix these confirmed security findings. For each: the finding JSON (id, code_ref, evidence, suggested_fix — hint not mandate), triage verdict. Rules: minimal-diff fixes; every P0 fix gets a regression test; run `pnpm typecheck && pnpm build && pnpm test` in affected packages until green; [if confirmed-spec-fix: apply EXACTLY this spec edit to <spec file>: <verbatim text>]; do NOT edit any other spec; commit per finding with message `fix(security): <id> <title>`. Findings: <JSON array>." 58min timeout; on timeout split cluster.
- [ ] **Step 3: Verify floors in worktree** (orchestrator): `pnpm typecheck && pnpm build && pnpm test` green; open each changed file and confirm the fix addresses the finding (verify-by-reading, never trust the claim).
- [ ] **Step 4: Code gate:** Opus `feature-dev:code-reviewer` subagent over the cluster diff (`git diff master...HEAD` in worktree), security-regression focus. Findings → fixer loop (back to Step 2 with gate feedback) until clean.
- [ ] **Step 5: Merge** worktree branch to master (`git merge --no-ff`), remove worktree.
- [ ] **Step 6:** After ALL clusters in the wave merged: process gate (code-review subagent at process level — direction, completeness vs FIXPLAN). Then next wave.

---

### Task 16: Rescan (iteration N+1)

**Wave:** 6 / **Blocks:** Task 17 / **Blocked by:** Task 15

- [ ] **Step 1:** Fresh scan worktree from updated master: `git worktree add .claude/worktrees/sec-scan-i<N+1> master`
- [ ] **Step 2:** Re-dispatch all 11 scanner prompts (Tasks 2–12 verbatim) with this prefix added: "RESCAN iteration <N+1>. Here are previously-found findings for your class with their fixes (JSON: <class JSON with triage>). For each confirmed-fixed finding: re-read the code path and verify the fix is real and complete — report status fixed|not-fixed|partial with fresh evidence. THEN hunt new instances of your class, especially regressions introduced by recent fix commits (git log --oneline -30 shows them). Write docs/plans/audit/security/<ID>.json with iteration <N+1>."
- [ ] **Step 3:** Collect, validate, commit as in Task 13 (`iteration: N+1`).

---

### Task 17: Convergence check (loop controller)

**Wave:** 7 / **Blocks:** Task 18 / **Blocked by:** Task 16

- [ ] Evaluate spec §7: all prior P0/P1/P2 verified `fixed` by rescan AND zero new P0/P1 AND gates clean →
  - **Converged:** new P2/P3 from rescan → one final hardening cluster via Task-15 procedure (no full re-iteration), then Task 18.
  - **Not converged AND iteration < 3:** triage new findings (Task 14 procedure) → Task 15 → Task 16 again.
  - **Iteration = 3:** stop; remaining findings → Task 18 as residual risk.

---

### Task 18: Final report

**Wave:** 8 / **Blocks:** — / **Blocked by:** Task 17

- [ ] **Step 1:** Write `docs/plans/audit/security/REPORT.md`: verdict per severity tier (P0: yes/no vulnerable + what was found/fixed; same per tier), full findings table (id, class, severity, status fixed/accepted-risk/residual/rejected), iteration history, live-probe outputs, spec updates made (file + section), product-intent questions for user (if any), follow-up recommendations (semgrep+gitleaks in CI — out of scope here).
- [ ] **Step 2:** Final process gate over REPORT (code-review subagent). 
- [ ] **Step 3:** Commit: `git add docs/plans/audit/security/REPORT.md && git commit -m "audit(security): final report"`. Clean up worktrees (`git worktree prune`).

---

## Self-review notes

- Spec coverage: §1 scope→S1-S11 focus lists; §3 matrix→Tasks 2-12; §4 schema→preamble; §5 triage→Task 14; §6 gates→Task 15 steps 3-6; §7 convergence→Task 17; §8 report→Task 18; §9 error handling→timeout/JSON-retry rules in Tasks 2-12/15; §10→regression-test rule in fix prompt.
- No same-wave file overlap: scanners write distinct JSONs; fix clusters proven disjoint in FIXPLAN before dispatch.
- TDD adaptation: regression test per P0 fix enforced in fixer prompt + code gate (this is an audit-orchestration plan; scanners produce findings, not code).
- Advisor unavailable in session → process gate substituted with Opus code-review subagent (documented in Tasks 14/15/18).
