Module 20 — Phase 5: Final Security Audit
This is a final, targeted security pass before v1.0 — not a repeat of
Module 15's dedicated security hardening review (#344/#359), Module 17's
dedicated SOC/threat-intel security review (#492), Module 18's full
platform-wide security sweep (Phases 9-28, covering auth, rate limiting,
file/storage, SSRF centralization, AI safety, audit log integrity,
security headers, dependency audit), or Module 19's monetization security
review (0023-module-19-monetization-security-review.md). Those reviews
remain valid; this pass audits whether anything has regressed since, and
does one focused new sweep (authorization/IDOR) prompted by the real
defect found in Phase 1.
Authorization / IDOR — new targeted sweep
Prompted by finding a real IDOR in ListApiKeysHandler during Phase 1
(a workspaceId filter used without a membership check — see
0024-module-20-backlog-reconciliation.md), ran a codebase-wide sweep
for the same bug class: any query/command handler that accepts a
workspaceId/organizationId directly from the client (not resolved
implicitly from the caller's own identity) and uses it in a Prisma call
without a membership check anywhere in its call chain.
Method: grepped all 226 non-spec files under
apps/api/src/modules/*/{queries,commands}/*.ts for workspaceId/
organizationId (138 raw matches), excluded every file that already
calls resolveWorkspaceMembership/resolveOrganizationMembership/
requireEnterpriseRole/requireRole (51 files — Module 9's
workspace-explicit surface, built with the check from the start), then
excluded every file using this codebase's implicit-resolution helpers
(resolveWorkspaceForCaller, findPersonalWorkspaceForUser,
resolveTargetForCaller, resolveBugBountyProgramForCaller,
resolveBugReportForCaller, resolveBugBountyFindingForCaller,
resolveVulnScanJobForCaller, AiContextService.resolveWorkspaceAndProject,
local resolveWorkspaceId/workspaceResolver.resolve — all of which
derive the workspace from the caller's own identity, then separately
verify any client-supplied child ID, like a projectId or targetId,
actually belongs to that resolved workspace before use). This left 23
files, each read in full.
Result: no further instances found. The ListApiKeysHandler fix from
Phase 1 was the only real instance of this bug class in the codebase; the
remaining 23 files were confirmed as false positives (implicit
caller-derived workspace resolution, a hardcoded literal, or an
intentionally-unauthenticated share-link flow that cross-checks the
resource's own workspace before serving anything). Full per-file
reasoning is preserved in the audit transcript; not duplicated here to
avoid a second copy of the same detail.
Authentication, SSRF, file security, AI security, billing security, secrets
Not re-audited from zero this module — each was already the subject of a dedicated pass:
- Authentication (session security, JWT, refresh rotation, MFA, OAuth state/CSRF, password reset, rate limiting): Module 1's original implementation plus fixes carried in the very first session of this project's history (refresh-token rotation race, OAuth state parameter, MFA-disable requiring current password), confirmed unchanged and not regressed by any Module 15-20 work (no auth-module files touched since).
- SSRF: centralized in Module 18 Phase 12 (
docs/operations/— network security centralization). No new user-configurable-endpoint surface was added in Modules 19-20 (Billing/API Keys/Org Switcher touch no outbound-request code). - File security (uploads/downloads/attachments/evidence/exports): Module 18 Phase 11. No new file-handling code was added in Modules 19-20.
- AI security (prompt injection, untrusted evidence, tool calling, approval gates): Module 18 Phase 13 (regression tests), Module 17 #493 (threat-intel-specific boundaries). No new AI code path was added in Modules 19-20.
- Billing security: Module 19's dedicated review
(
0023-module-19-monetization-security-review.md) covers IDOR, price/ plan trust boundary, webhook signature/idempotency, quota concurrency, AI-credit double-deduction, admin-endpoint separation, read-only usage, and frontend secret exposure in full. Re-confirmed still accurate — no billing code changed since except this module's Organization-switcher addition, which is frontend-only and introduces no new backend trust boundary (see Phase 4 write-up). - Secrets: repeated the
API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEYclass of grep across the repository, scoped to source (notnode_modules/dist/.git). No committed secrets found. The only matches are: environment-variable names in.env.example/.env.enterprise.example(placeholders, no values), CI workflow env vars using obvious placeholder test values (e.g.JWT_ACCESS_SECRET: "ci-test-jwt-access-secret-please-change-32chars"in.github/workflows/release.yml, already noted in the Phase 0 baseline audit), and source code that references secret env var names (process.env.STRIPE_WEBHOOK_SECRET, etc.) without embedding a value — the expected, correct pattern.
Summary
One real authorization defect (IDOR) was found and fixed this module (Phase 1); a full-codebase sweep for the same bug class found no further instances. All other Phase 5 sub-areas were confirmed unchanged from their most recent dedicated audits, with no new code introduced in Modules 19-20 that would create a new instance of any previously-checked vulnerability class.