OWASP Top 10 Review skill for Claude Code
Findings with file:line evidence, not vague advice. Turns a vague 'is this secure?' into a structured source review. Claude maps every entry point, sweeps each OWASP Top 10:2025 category with targeted searches, confirms each lead by tracing attacker input to the sink, and only then reports. Findings come with severity, a concrete attack path, the fix, and an honest list of what was not reviewed.
- name
owasp-review - size174 lines · ≈2,200 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Reviews application code against the OWASP Top 10 (2025) with concrete, greppable checks per category, traces untrusted input to dangerous sinks, rates each finding by exploitability and impact, and writes a findings report with file:line evidence and fixes. Use when asked for a security review, OWASP audit, pre-release security check, or "is this code secure", or when reviewing a PR that touches auth, queries, file handling, or access control. Try asking
- Do an OWASP Top 10 review of the api/ folder before we launch
- Review this PR for security issues — it touches the invoice download route
- Check our Django app for injection and broken access control
Use it when
- A release or audit needs a written security review of source code
- A PR changes authentication, authorization, queries, or file handling
- You inherited a codebase and need to know its worst security risks
- A pentest or scanner report needs triage against the actual code
Skip it when
- You need a design-level risk analysis before code exists — use threat-modeling instead
- The question is only about vulnerable dependencies — use supply-chain-audit instead
How to check the result
Every finding points to a real file and line at the reviewed commit, each High or Critical has a test that fails before the fix, and re-running the category searches after fixes shows no unexplained hits.
The common mistake it prevents
Reporting raw grep hits as vulnerabilities without tracing whether attacker-controlled input actually reaches the sink, which buries the real issues in false positives.
A review is done when every finding has file:line evidence, a plausible attack path from an untrusted source, a severity you can defend, and a concrete fix. “Consider validating input” is not a finding. Silence on a category means you checked it, not that you skipped it.
Ground rules
- Review only code the user owns or is authorized to assess. Do not probe live systems; this is a source review.
- Trace data flow: source (request, file, queue message, env, third-party API) to sink (query, shell, template, filesystem, redirect, deserializer, authz decision).
- A pattern match is a lead, not a finding. Confirm reachability and that no upstream control already neutralizes it before reporting.
- Report what you did not cover (generated code, vendored deps, infra) so nobody assumes it was reviewed.
Workflow
- [ ] 1. Scope: stack, entry points, trust boundaries, what is out of scope
- [ ] 2. Map the attack surface (routes, handlers, jobs, webhooks, CLIs)
- [ ] 3. Identify the auth and authorization model
- [ ] 4. Sweep each Top 10 category with targeted searches
- [ ] 5. Confirm each lead by tracing source to sink
- [ ] 6. Rate severity, write fixes, de-duplicate root causes
- [ ] 7. Produce the report and list coverage gaps
1–2. Scope and attack surface
Find every place untrusted data enters:
# HTTP routes (adjust for the framework in use)
grep -rnE "@(app|router|bp)\.(get|post|put|patch|delete|route)|app\.(get|post|put|patch|delete)\(|@(Get|Post|Put|Patch|Delete)Mapping|Route::(get|post)" --include=*.{py,js,ts,java,php,rb,go} .
# Webhooks, queue consumers, scheduled jobs
grep -rniE "webhook|consumer|subscribe|on_message|@celery|@shared_task|cron" --include=*.{py,js,ts,go,java,rb} .
Write a one-line inventory per entry point: method, path, auth required (yes/no/which role), inputs. Unauthenticated and admin endpoints get the deepest review.
3. Auth model
Answer before sweeping: where is identity established (middleware, decorator, gateway)? Where is authorization decided (per route, per object, policy layer)? Is there a default deny? Endpoints that bypass the shared middleware are the first place to look.
4. Category sweep (OWASP Top 10:2025)
| ID | Category | What to check first |
|---|---|---|
| A01 | Broken Access Control | Object lookups by user-supplied ID without owner/tenant filter; missing role checks on admin routes; CORS with credentials and reflected origin; SSRF (now folded in here) via user-supplied URLs |
| A02 | Security Misconfiguration | Debug mode in prod config, default credentials, verbose error pages, permissive CORS, missing security headers, XXE-enabled XML parsers |
| A03 | Software Supply Chain Failures | Unpinned deps, no lockfile, install scripts, abandoned packages, CI pulling unverified actions by tag |
| A04 | Cryptographic Failures | MD5/SHA1 for passwords, ECB mode, hardcoded keys, Math.random()/random for tokens, TLS verification disabled |
| A05 | Injection | String-built SQL/NoSQL/LDAP/shell, template injection, unescaped output (XSS), eval on input |
| A06 | Insecure Design | No rate limit on login/OTP, business logic that trusts client totals or state transitions, missing abuse limits |
| A07 | Authentication Failures | Weak password hashing, no lockout/throttle, session not rotated at login, tokens in URLs, user enumeration |
| A08 | Software or Data Integrity Failures | Unsafe deserialization (pickle, Java native, YAML load), unsigned updates/webhooks, client-trusted hidden fields |
| A09 | Security Logging and Alerting Failures | Auth failures and access denials not logged, secrets/PII in logs, no alerting hooks |
| A10 | Mishandling of Exceptional Conditions | Fail-open error paths (exception grants access), swallowed errors, stack traces to clients, unchecked resource exhaustion |
High-yield searches (confirm every hit manually):
# A05 Injection
grep -rnE 'execute\(\s*f["'"'"']|execute\([^)]*%|execute\([^)]*\+|\.raw\(|whereRaw|literal\(' .
grep -rnE '\$\{[^}]+\}.*(SELECT|INSERT|UPDATE|DELETE|WHERE)' --include=*.{js,ts} .
grep -rnE "subprocess\.(call|run|Popen)\([^)]*shell=True|os\.system\(|child_process\.exec\(|Runtime\.getRuntime\(\)\.exec" .
grep -rnE "dangerouslySetInnerHTML|innerHTML\s*=|v-html|\|\s*safe\b|mark_safe\(|html_safe|<%==|\{\{\{" .
# A08 Deserialization (yaml.load is fine only with Loader=SafeLoader)
grep -rnE "pickle\.loads?|yaml\.load\(|ObjectInputStream|unserialize\(|Marshal\.load|BinaryFormatter" .
# A04 Crypto
grep -rniE "md5|sha1\(|AES/ECB|Math\.random\(|random\.random\(|verify\s*=\s*False|rejectUnauthorized:\s*false|InsecureSkipVerify:\s*true" .
# A01 SSRF and redirects
grep -rnE "requests\.(get|post)\(\s*(url|request)|fetch\(\s*req\.|axios\.(get|post)\(\s*req\.|redirect\(\s*req(uest)?\.(query|args|GET)" .
# A02 Debug/config
grep -rnE "DEBUG\s*=\s*True|app\.run\(.*debug=True|NODE_ENV.*development|Access-Control-Allow-Origin.*\*" .
For the full per-category checklist with framework-specific patterns and safe equivalents, read references/category-checks.md when a category has hits or the stack is unfamiliar.
5. Confirm leads
For each candidate:
- Identify the source. Is it attacker-controlled (request body, query, header, cookie, uploaded file, third-party callback)?
- Walk every transformation between source and sink. Note validation, type coercion, parameterization, encoding.
- Decide: exploitable, exploitable with preconditions (auth, specific role, config), or not exploitable (state why — e.g. “id is cast to int on line 40”).
- For access control, write the concrete attack: “User A sends
GET /invoices/1043belonging to user B; handler loads by id only (invoices.py:88) and returns it.”
6. Severity
Rate with likelihood times impact; do not inflate.
| Severity | Typical examples |
|---|---|
| Critical | Unauthenticated RCE, SQLi on a public endpoint, auth bypass, cross-tenant data read at scale |
| High | Authenticated IDOR exposing other users’ PII, stored XSS in a shared view, SSRF to cloud metadata, unsafe deserialization behind auth |
| Medium | Reflected XSS needing user interaction, missing rate limit on login, CSRF on state-changing action, verbose errors leaking internals |
| Low | Missing hardening header with no direct exploit, weak but non-exploitable config, info disclosure of versions |
| Info | Defense-in-depth suggestions, code smells with no current path |
Group findings that share one root cause (e.g. a query helper that concatenates) into a single finding listing all call sites.
7. Report
# Security review: <repo/service> @ <commit>
**Scope:** <dirs/services reviewed> **Out of scope:** <what was not reviewed>
**Method:** Source review against OWASP Top 10:2025; no dynamic testing.
## Summary
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 2 |
| Medium | 3 |
| Low | 1 |
## Findings
### [H-1] IDOR on invoice download (A01 Broken Access Control)
**Location:** api/invoices.py:88 (`get_invoice`)
**Evidence:** `Invoice.objects.get(id=invoice_id)` with no owner/tenant filter; route
requires login only.
**Attack:** Any authenticated user can enumerate sequential IDs to download others' invoices.
**Fix:** Scope the query: `Invoice.objects.get(id=invoice_id, account=request.user.account)`;
return 404 (not 403) on miss. Add a test that user B gets 404 for user A's invoice.
**Also affects:** api/receipts.py:41, api/exports.py:17
## Categories with no findings
A04, A09 — checked: <one line on what was checked>.
## Coverage gaps
- Terraform and Kubernetes manifests not reviewed.
- Vendored `third_party/` not reviewed.
Gotchas
- ORMs are not automatically safe:
.raw(),.extra(),whereRaw,sequelize.literal, and string-builtorder bycolumns are common injection points. - Template auto-escaping does not cover attribute contexts without quotes,
hrefvalues (javascript:URLs), inline<script>blocks, or CSS. - Frontend role checks are UX, not security. Always find the server-side check.
- 403 vs 404 on unauthorized object access: 403 confirms existence. Prefer 404.
- SSRF filters on hostname strings are bypassable (DNS rebinding,
0x7f000001, IPv6, redirects). The fix is resolving and checking the IP at connect time, or an allowlist. - A lockfile with a vulnerable transitive dep is a supply-chain finding; defer depth to supply-chain-audit rather than duplicating it.
Verification
- Every finding cites a file and line that exists at the reviewed commit.
- For each High/Critical, propose a test (unit or integration) that fails before the fix.
- After fixes land, re-run the category searches and confirm the hits are gone or justified in a comment.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/category-checks.md
# OWASP Top 10:2025 — detailed checks and safe equivalents
Read the section for a category when the sweep in SKILL.md produced hits, or when the
stack is unfamiliar. Each section: what to look for, framework-specific traps, the fix.
## A01 Broken Access Control (includes SSRF)
Look for:
- Object fetched by ID from the request with no owner/tenant predicate.
- Authorization in the controller for some actions but not siblings (e.g. `update` checks,
`delete` does not). Compare handlers in the same resource side by side.
- Bulk/list endpoints that filter correctly but export/download endpoints that do not.
- Role checks on string compare of a client-supplied claim or header (`X-User-Role`).
- Path-based checks that can be bypassed by case, trailing slash, `;`, or encoded `/`.
- CORS: `Access-Control-Allow-Origin` reflecting the request `Origin` with
`Access-Control-Allow-Credentials: true`.
- SSRF: server fetches a user-supplied URL (webhooks, image import, PDF rendering,
link previews, OIDC discovery URLs).
Safe patterns:
- Scope queries through the principal: `current_user.invoices.find(id)` (Rails),
`Invoice.objects.filter(account=request.user.account).get(pk=pk)` (Django),
`where: { id, tenantId: session.tenantId }` (Prisma).
- Central policy layer (Pundit, django-rules, CASL, OPA) with default deny; one test per
action per role.
- SSRF: allowlist destinations; otherwise resolve DNS, reject private, loopback,
link-local (169.254.0.0/16, fd00::/8, ::1), and re-check after every redirect; disable
redirects or cap them; use IMDSv2 on AWS.
## A02 Security Misconfiguration
Look for: `DEBUG=True`, Flask `debug=True`, Spring Boot actuator endpoints exposed
(`management.endpoints.web.exposure.include=*`), Express without `app.disable('x-powered-by')`
(minor), default admin passwords in seeds used in prod, stack traces rendered to users,
directory listing enabled, XML parsers with external entities enabled.
XXE fixes:
- Python: use `defusedxml` instead of `xml.etree`/`lxml` for untrusted XML.
- Java: `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)`.
- .NET: `XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit }`.
## A03 Software Supply Chain Failures
Look for: missing lockfile, `npm install` instead of `npm ci` in CI, `pip install` without
hashes or pins in Docker builds, GitHub Actions referenced by mutable tag (`@v3`) for
third-party actions, `curl | sh` in Dockerfiles, packages with install scripts from
unknown publishers. Hand deeper analysis to the supply-chain-audit skill.
## A04 Cryptographic Failures
Look for:
- Password hashing with anything other than argon2id, scrypt, bcrypt, or PBKDF2 with a
high iteration count.
- Tokens, reset codes, session IDs from non-CSPRNG sources.
Safe: `secrets.token_urlsafe(32)` (Python), `crypto.randomBytes(32)` / `crypto.randomUUID()`
(Node), `SecureRandom` (Java).
- AES-ECB, AES-CBC without a MAC, static IVs/nonces, keys in source.
- TLS verification disabled (`verify=False`, `rejectUnauthorized: false`,
`InsecureSkipVerify: true`).
- Sensitive data stored unencrypted where policy requires encryption at rest.
Hand design questions to cryptography-basics.
## A05 Injection
SQL:
- Unsafe: f-strings, `%`, `+`, template literals into `execute`, `query`, `raw`, `whereRaw`,
`sequelize.query`, `knex.raw`, JPA `createQuery` with concatenation.
- Identifiers (column names in `ORDER BY`, table names) cannot be parameterized — map
user input through an allowlist dict.
NoSQL:
- Mongo queries built from raw JSON bodies allow operator injection (`{"$ne": null}`).
Validate types (string expected → reject objects) or use `mongo-sanitize`-style
stripping of `$`-prefixed keys.
OS command:
- Never `shell=True` with input. Use argument arrays: `subprocess.run(["convert", path, out])`,
`execFile("convert", [path, out])`. Still validate that `path` cannot start with `-`
(option injection); pass `--` before positional args when the tool supports it.
XSS:
- React: `dangerouslySetInnerHTML`, `href={userUrl}` (javascript: URLs), server-rendered
JSON in `<script>` without escaping `<`.
- Vue `v-html`, Angular `bypassSecurityTrust*`, Jinja `|safe`, Django `mark_safe`,
Rails `html_safe`/`raw`, Handlebars `{{{ }}}`, EJS `<%-`.
- Fix: keep auto-escaping, sanitize HTML with DOMPurify (client) or a maintained server
sanitizer (bleach is deprecated; prefer `nh3` in Python), validate URL schemes.
Template injection (SSTI): user input passed as the template itself
(`render_template_string(user_input)`, `new Function`, `eval`).
## A06 Insecure Design
Look for business-logic flaws a scanner cannot find:
- Price, quantity, discount, or role accepted from the client.
- State machines that allow skipping steps (pay → ship without paid check).
- No per-account and per-IP limits on login, OTP, password reset, signup, invite, export.
- Race conditions on balances, coupons, inventory (check-then-act without a lock or
conditional update).
Fix with server-side recomputation, explicit state transitions, rate limits, and
atomic conditional updates (`UPDATE ... SET balance = balance - :x WHERE balance >= :x`).
## A07 Authentication Failures
Look for: session ID not regenerated after login, logout that does not invalidate the
server session, password reset tokens that do not expire or are reusable, different
error messages or timing for unknown user vs wrong password, MFA that can be skipped by
calling the next endpoint directly, JWT accepted without signature/exp/aud checks. Hand
depth to auth-sessions and jwt-handling.
## A08 Software or Data Integrity Failures
Unsafe deserialization of untrusted data:
- Python `pickle`, `shelve`, `yaml.load` without `SafeLoader`, `jsonpickle`.
- Java `ObjectInputStream`, XStream without allowlist, Jackson default typing.
- PHP `unserialize`, Ruby `Marshal.load`, `YAML.load` (use `YAML.safe_load`).
- .NET `BinaryFormatter` (obsolete; removed from .NET 9 runtime).
Fix: use JSON with schema validation; sign data you must round-trip (HMAC) and verify
before parsing. Webhooks: verify signatures with constant-time compare.
## A09 Security Logging and Alerting Failures
Check that these events are logged with user ID, IP, timestamp, and request ID: login
success/failure, MFA events, password change/reset, permission denials, admin actions,
input validation failures on sensitive endpoints. Check that passwords, tokens, full card
numbers, and session cookies are never logged (search logger calls near `password`,
`token`, `authorization`).
## A10 Mishandling of Exceptional Conditions
Look for:
- `try: check_permission() except: pass` — fail-open authorization.
- Default branches that grant access (`else: return True`).
- Errors returning raw exception text or stack traces to clients.
- Unbounded reads (request bodies, decompression, regex on attacker input — ReDoS).
- Partial failures that leave inconsistent state (charged but not recorded).
Fix: fail closed, catch narrowly, return generic messages with a correlation ID, set
body/size/time limits.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.