owasp-audit
Audit application source code against the OWASP Top 10 (2021) vulnerability categories — broken access control, cryptographic failures, injection, insecure design, security misconfiguration, vulnerable components, authentication failures, data integrity, logging failures, SSRF. Use when the user men
By briiirussell · 392 installs
npx skills add briiirussell/cybersecurity-skills --skill owasp-audit
Source repository · Upstream listing
OWASP Audit — Source Code Security Review
Perform a systematic security audit of application source code against the OWASP Top 10:2025. Every candidate is
traced, then independently challenged, before it becomes a finding. Coverage is recorded per surface and trust
boundary, so "clean" means someone actually read the code.
Cross references:
api audit for a surface driven pass over every API endpoint (shares the recon, verification, and orchestration references)
web pentest "Combined white box + black box mode" when you have both the source and a running instance you're authorized to test
business logic testing to prove race conditions and logic flaws dynamically (A06)
dependency audit for the full A03 supply chain picture; secrets audit for leaked credentials
threat modeling for the principal / trust boundary vocabulary used in recon
finding triage to take a single finding from this report to a defensible disposition
Authorization and Mode
Only audit code the user owns or is authorized to review. Any live check (for example curl I against a deployed
origin) needs the same authorization for that host and stays non destructive.
Pick the mode before starting:
Guidance (default): a question, a single file, or "how do I secure X". Answer directly. Findings still follow the
verdict rules below. No ledger required.
Full audit : the user explicitly asks to audit or security review a codebase, a release, or a PR. Run the full
workflow below.
Diff / PR scoped : the user wants a branch, PR, or commit range reviewed, or a pre merge gate. Run the full workflow
limited to the change, using [references/diff review.md](references/diff review.md).
If it's ambiguous, ask one question: guidance pass or full audit (and which profile)?
Workflow (full audit)
1. Scope : what's in, what's out, mode, and run profile
2. Recon : write architecture.md and coverage.json before hunting
3. Hunt : work the category checklist unit by unit (subagents for large codebases)
4. Verify every finding : an independent refutation pass gives each candidate one verdict
5. Coverage critic : look for unmapped surfaces and thin coverage
6. Write and validate findings.json
7. Render the report from the JSON
Write everything to security audit/ at the repo root (ask whether to commit or gitignore it). Re runs read the prior
files from there.
Step 1 — Scope
Language, framework, runtimes, deploy target
In scope / out of scope, with a reason for each exclusion
Mode (full or diff) and run profile : quick , standard (default), or deep . Profiles change breadth and
redundancy, never the evidence bar. See [references/orchestration.md](references/orchestration.md).
Prior run? Load the previous findings.json and coverage.json and carry forward by fingerprint (orchestration.md
"Re runs and carry forward"). Never re raise a rejected claim whose control is unchanged.
Step 2 — Recon (read only)
Before grepping for vulnerabilities, map the system ([references/recon and coverage.md](references/recon and coverage.md)):
Every lower trust principal (anonymous, user, other tenant, webhook sender, job runner) and the file:line where its
controls live
Entry surfaces: routes, server actions, loaders, tRPC procedures, GraphQL resolvers, webhooks, jobs, cron, uploads
Sinks: DB writes/raw queries, exec, filesystem, outbound HTTP, HTML rendering, email, paid third party calls, redirects
What's deployed vs what's in the repo (proxy, CDN, IdP, env). These become needs validation blockers later
Frameworks and BaaS. Match package.json dependencies and config files against the detection table in
[references/frameworks/README.md](references/frameworks/README.md) (for example next + next.config. → Next.js,
@supabase/ or supabase/config.toml → Supabase). Load only the matching playbooks, add their surfaces to the
ledger, and work their checks in Step 3. Their findings use the same categories, verdicts, and format as the rest
Write architecture.md (≤ ~1,000 words) and seed coverage.json with one planned unit per surface × boundary ×
OWASP category.
Step 3 — Hunt
Work through the [Audit Checklist]( audit checklist) per ledger unit , not per category in the abstract. For each
unit, read the entrypoint, follow data to every sink, and record what you checked even when nothing is wrong.
Large codebase: split units across isolated hunter subagents, unauthenticated surfaces first. Only the coordinator
writes coverage.json / findings.json . Hunter prompt, result shape, and the sequential fallback are in
[references/orchestration.md](references/orchestration.md).
Source and a running app available (and authorized): use web pentest "Combined white box + black box mode" so
code findings guide live tests and live anomalies prioritize code review.
A candidate needs a lower trust principal, the input they control, the control that should stop them, the boundary
crossed, the affected resource, and a concrete result. Missing any of these, it's a hardening note or needs
validation, not a finding.
Step 4 — Verify every finding
Before anything reaches the report, run the per finding refutation pass in
[references/verification.md](references/verification.md). One fresh verifier per candidate (a subagent where
supported; otherwise a separate step that doesn't re read the hunting notes). It re reads every cited file:line ,
names the strongest control on the path, and returns exactly one verdict. Apply the fixed exclusion rules (test,
generated, vendored, docs) first. In Claude Code, dispatch the plugin's read only finding verifier subagent per candidate.
Verdict Bar Severity
confirmed Complete entrypoint → control → sink → impact trace, or a PoC Yes, per [references/severity calibration.md](references/severity calibration.md)
needs validation Code supports it; one decisive fact lives outside the code. Requires blockers[] + an owner run validation plan No
rejected A named control at a named location runs on every reachable path and fails closed No; kept so re runs don't re raise it
Rules that matter most:
"Potential" is retired. Use Needs validation , and only with a concrete blocker and plan. It's not a parking place
for a hunch.
Library trust, a control on a different path, a control at the wrong time, a fail open control, a safe sibling,
missing information, difficulty, "operator configurable", and "internal" do not rule a candidate out.
Record counterevidence on every verdict. A static only trace caps confidence at medium.
An upgrade (higher verdict or severity) needs a new verifier.
Gaps with no traced impact close as hardening notes (not findings, no severity).
Step 5 — Coverage critic
Before writing the report, run the critic in [references/recon and coverage.md](references/recon and coverage.md)
over architecture.md + coverage.json : unmapped entry points, unchecked sibling paths, covered units with thin
checks, unjustified exclusions, candidates without verdicts. Add missing units and review them (or mark them
deferred with a reason). covered requires non empty reviewed paths and at least one check.
Step 6 — Write and validate findings.json
Write every verdict record to security audit/findings.json (sorted by fingerprint; format in
[references/findings format.md](references/findings format.md)), then validate both files:
Fix every validation error before writing prose. Fingerprints name the root cause and never include line numbers,
severity, or verdict. Optional SARIF export for GitHub code scanning: references/to sarif.mjs (see findings format.md).
Step 7 — Render the report
Render report.md from findings.json , coverage.json , and hardening notes.md using the [Output Format]( output format).
The JSON is the source of truth: prose never changes a verdict, severity, blocker, or demonstrated impact. If they
disagree, fix the JSON, re validate, and re render.
Audit Checklist
For each unit, grep for the patterns below, then read the flagged files and trace them.
Categories follow the OWASP Top 10:2025 . Tag findings and coverage units A01:2025 … A10:2025 . Mapping for reports that
used the 2021 list: A01 → A01 (SSRF, 2021's A10, is now part of A01); Cryptographic Failures A02 → A04; Injection A03 → A05;
Insecure Design A04 → A06; Security Misconfiguration A05 → A02; Vulnerable Components A06 → A03 Software Supply Chain
Failures (broader); A07 and A08 keep their numbers; A09 is now Logging and Alerting Failures; A10 Mishandling of
Exceptional Conditions is new.
A01: Broken Access Control
Missing authorization checks on endpoints or routes
IDOR — user controlled IDs without ownership verification
Auth check ordering. Verify the authorization check runs before any branch that can reveal whether the resource exists, what state it's in, or any other resource specific metadata. Returning 404 for "not found", 400 for "wrong state", and 401 for "not authenticated" is itself a leak — an attacker enumerates resource IDs and learns states without ever passing the auth gate. Recommended response shape: uniform 404 for everything an unprivileged caller should not see.
Framework RPC surfaces that don't appear as routes. Server actions and equivalents are publicly exposed RPCs that file scans miss. Enumerate and audit each one for auth + ownership:
Next.js: every exported function in a file with 'use server'
Remix / React Router: every action / loader export
tRPC: every procedure
GraphQL: every resolver
Rails: non resource controller actions
IDOR via foreign keys in mutation payloads. Form posts a foreign key UUID ( categoryId , projectId , teamId , organizationId ) → server validates ownership of the primary record but blindly accepts the FK → ORM relation join later surfaces another tenant's data. Look for formData.get("<id ") / body.<id passed straight to insert/update without a preceding findFirst({ where: { id, userId } }) . For ORM relation joins (Drizzle with: , Prisma include , ActiveRecord includes ), trace whether the join target is filtered by the same tenant/ownership predicate as the parent query.
Inbound webhooks authenticate the caller before DB or paid work. Email, SMS, payment, and git webhooks that identify the account from the payload ( to address, slug) but never verify the provider's signature over the raw body let anyone trigger paid work and create records in another account. Provider signature table and raw body capture: [references/webhooks and parsers.md](references/webhooks and parsers.md 2 inbound webhook triggers paid work without authenticating the caller a01 a07). Grep: inbound , parse , sendgrid , mailgun , postmark , /api/. email , multer( , bodyParser: false .
Missing CSRF protections on state changing requests
Role checks only on the frontend, not enforced server side
Open redirect via post auth return to parameter — ?from= , ?next= , ?returnTo= , ?continue= , ?redirect= passed unsanitized to redirect() / Response.redirect() . Restrict to same origin paths under the expected scope, normalize ( new URL(target, "http://localhost").pathname ) to defeat traversal like /admin/../foo . Also reject control bytes in the path before redirect: tab/newline/null ( \t , \n , \0 ) — URL parsers strip these and collapse /\tevil into protocol relative //evil ; null bytes can turn the redirect into a 500. Reject any byte in [\x00 \x1F\x7F] , any backslash, and any percent encoded slash/backslash ( %2f , %5c ).
Grep for: direct object references, missing auth middleware, user ID from request params, redirect(. from , redirect(. next , redirect(. returnTo
SSRF (A10 in 2021; part of A01 since 2025)
User controlled URLs passed to server side HTTP requests
Missing