--- name: auditing-code-for-vulnerabilities description: Audit source code for exploitable vulnerabilities using threat-model-driven review, taint tracing, invariant checking, and variant analysis. Use when reviewing a codebase or diff for security bugs, performing a security audit, hunting for vulnerabilities in a target's source, or validating whether a suspected finding is real. verified: 2026-07-28 --- # Auditing Code for Vulnerabilities Finding real bugs in source code is a different job from running a scanner. A scanner matches patterns; an auditor builds a model of what the code is *supposed* to guarantee and then hunts for the paths where that guarantee breaks. This skill is the methodology for the second job. ## When to Use - Auditing a repository, service, or library for security defects - Reviewing a diff, branch, or pull request for introduced vulnerabilities - Hunting for a specific bug class across a large codebase - Validating whether a scanner finding or a reported vulnerability is real - Building an audit plan and coverage report for a client engagement ## When NOT to Use - **Black-box testing of a running app** — use `testing-web-applications` or `testing-apis` - **A PHP codebase specifically** (type juggling, unserialize/phar POP chains, php:// wrappers) — this methodology plus `auditing-php-applications` - **Hunting a planted webshell or backdoor** rather than a vulnerability — use `hunting-web-backdoors` - **Binary-only targets** — use `analyzing-binaries` - **Dependency and build-pipeline risk** — use `auditing-supply-chain` - **Cryptographic construction review** — use `reviewing-cryptography` - **Writing up finished findings** — use `reporting-security-findings` - **Running a multi-agent discovery campaign** across a whole target with builder/critic separation and iteration — use `orchestrating-vulnerability-research`, which dispatches *this* skill as the per-slice hunter ## The Loop Auditing is four passes, not one. Do not skip to pass 3. ``` 1. Context → what does this system protect, and from whom? 2. Attack surface → where does untrusted input enter, and what does it reach? 3. Hunt → trace specific bug classes along those paths 4. Verify → prove exploitability before you write a word ``` ### Pass 1: Build context before reading code Do not open files at random. Spend the first block of effort answering: | Question | Where to look | | --- | --- | | What are the security-relevant assets? | README, docs, data models, DB schema | | Who are the actors and trust tiers? | Auth middleware, role enums, tenant models | | What are the stated invariants? | Tests, assertions, comments containing "must", "never", "invariant" | | What has already been fixed here? | `git log --grep` for security keywords, CVE files, SECURITY.md | | What is out of scope? | Engagement brief, vendor code, generated files | ```bash # Prior security work is the cheapest source of bug leads git log --oneline --grep='security\|CVE\|vuln\|injection\|auth bypass\|overflow' -i | head -40 # Stated invariants often mark where the author was nervous rg -n --stats 'MUST NOT|must never|SECURITY|XXX|HACK|TODO.*(auth|secur|valid)' -i # Where does privilege actually get checked? rg -n 'is_admin|require_role|authorize|has_permission|@login_required|checkAccess' ``` Write a short target model before hunting: assets, actors, trust boundaries, and the three invariants whose violation would matter most. Everything after this is a search for counterexamples to those invariants. ### Pass 2: Map the attack surface Enumerate *entry points*, then rank them. An entry point matters in proportion to how far it reaches before it is validated. ```bash # HTTP/RPC routes rg -n '@(app|router)\.(get|post|put|delete|patch)|app\.(get|post)\(|@RequestMapping|http\.HandleFunc' # Deserialization, template rendering, and dynamic execution sinks rg -n 'pickle\.loads|yaml\.load\(|Marshal|unserialize|ObjectInputStream|eval\(|new Function|exec\(|Runtime\.getRuntime' # Command, SQL, and path sinks rg -n 'os\.system|subprocess.*shell\s*=\s*True|child_process\.exec\(|execSync|Statement\.execute|\.raw\(|fmt\.Sprintf.*SELECT' # Where authentication is decided rather than enforced rg -n 'verify=False|InsecureSkipVerify|jwt\.decode\(.*verify.*False|algorithms=\[.*none' ``` Rank entry points by: reachable without authentication > reachable by a low privilege tier > reachable only by an admin. Then follow the highest-ranked ones inward. Depth beats breadth — one fully traced path is worth twenty grep hits. **Grep hits are candidates, not findings.** The commands above are a cheap wide net; each match is an unresolved lead until you have traced it. Persist the candidate set — a worklist of `(file:line, bug class, entry point)` — and drive every entry to an explicit verdict: *traced-safe*, *confirmed*, or *needs-PoC*. Widen the net cheaply, then spend expensive reasoning per candidate — never the reverse. The failure mode is not a missing grep pattern; it is enumerating fifty candidates, eyeballing five, and calling the tree clean. On a large codebase, fold the project's own conventions into the net — its ORM's raw-query escape hatch, its auth decorator's name, its templating call — because the highest-yield sinks are the ones generic patterns miss. ### Pass 3: Hunt bug classes along the traced paths For each promising path, trace taint from source to sink and ask what the code assumes. The high-yield classes, in rough order of how often they survive to production: **Authorization, not authentication.** Most real breaches are missing object level checks, not broken login. For every handler that takes an ID, ask: is the object scoped to the caller's tenant/user, or only looked up by ID? Check the query, not the decorator. **Trust-boundary confusion.** Data validated at one layer and re-parsed at another. Look for values that cross a serialization boundary — a validated string re-parsed as a URL, a path, a template, or a query. **State and concurrency.** Check-then-use gaps, non-atomic balance updates, idempotency keys that are not actually unique, retry paths that replay side effects. Search for reads followed by writes with no lock or transaction. **Injection into a secondary interpreter.** SQL, shell, LDAP, XPath, template engines, log formats, and regex. The question is never "is there a filter" but "does the filter and the interpreter agree on the grammar." **Memory safety** (C/C++/unsafe Rust/CGo). Length arithmetic before bounds checks, `memcpy` with an attacker-influenced size, off-by-one in loop bounds, signed/unsigned conversions, use-after-free on error paths. **Error and cleanup paths.** The happy path is usually reviewed; the `except`, `catch`, `defer`, and `goto fail` branches are not. Audit them specifically. **Secrets and cryptographic misuse.** Hardcoded keys, non-constant-time comparison of tokens, predictable IDs from `Math.random`/`rand()`, missing signature verification. Deep crypto review belongs in `reviewing-cryptography`. ### Pass 4: Variant analysis A bug is a template, not an incident. When you confirm one, immediately search for its siblings — the same mistake made by the same author, the same copied block, the same missing check on a neighbouring route. ```bash # You found one unscoped lookup. Find every other one. rg -n 'find_by_id|findOne\(\{ *_id|get_object_or_404' -A3 | rg -v 'tenant|owner|user_id' ``` Variant analysis is where audits produce disproportionate value. Budget time for it explicitly — roughly one unit of variant search per confirmed finding. ## Verification Before Reporting A finding you cannot demonstrate is a hypothesis. Before it goes in the report, answer all four: 1. **Reachability** — name the concrete entry point and the caller privilege required. "An attacker who can reach `POST /api/export` unauthenticated." 2. **Control** — show which part of the dangerous value the attacker controls. 3. **Impact** — state what breaks: which invariant from Pass 1, and what an attacker gains. 4. **No mitigating control** — check for a WAF rule, a framework default, a middleware, a DB constraint, or a caller that already sanitizes. If a proof of concept is in scope, write the smallest one that proves control of the sink — not a weaponized exploit. ### Revalidate to prune false positives The four checks above confirm a finding; this pass tries to *kill* it. Run it on every confirmed candidate before it reaches the report — a report's credibility is set by its worst false positive, not its best true finding. - **Is it already fixed?** The tree you are reading may lag the fix, or the fix may sit on a branch you have not pulled. Confirm the vulnerable code is what actually ships before you file it. ```bash git log -S'' --oneline -- # when this line changed, and toward what git log --oneline ..origin/main -- # a fix on main you are not reading git blame -L , # the commit that introduced it, for context ``` - **Re-derive it adversarially.** Argue the opposite case: assume the code is safe and go find the control that makes it so — the middleware, the DB constraint, the caller that already sanitizes. A finding that survives a genuine attempt to disprove it is one you can defend. - **Confirm the sink still receives your value.** Re-trace the last hop. A refactor often slips a validator or an encoder between source and sink that a first read glides past. Drop what dies here, and say so in your coverage notes. A candidate you cannot revalidate is a note to yourself, not a finding. ## Rationalizations to Reject These are the thoughts that turn an audit into a formality. Each one is wrong. - *"It's probably validated upstream."* Then go read upstream. Unverified assumptions about a caller are the single most common source of missed bugs. - *"The framework handles that."* Frameworks handle the default path. Check the version, check the config, check whether this call uses the safe API. - *"That input is internal."* Internal today. Trace how it is populated; a queue consumer or admin import is usually reachable from outside. - *"It's only exploitable by an authenticated user."* That is a severity input, not a reason to drop the finding. - *"It looks intentional."* Intent is not a control. Note the intent, keep the finding. - *"The scanner didn't flag it."* Scanners find what they have rules for. - *"I confirmed it, so it's real."* You confirmed it against one checkout and your own first read. Revalidate it against what actually ships and against a genuine attempt to disprove it before it goes in the report. - *"I've reviewed enough files."* Coverage is measured against the attack surface you mapped in Pass 2, not against file count. ## Tool Assist, Not Tool Substitute Static analysis is for coverage and for variant search after you know the pattern. Write a rule once you have a confirmed bug, and let it find the rest. ```bash # Semgrep: broad pass, then a rule you write for your specific finding semgrep --config=auto --severity=ERROR --json -o semgrep.json . semgrep --config=./rules/my-variant-rule.yaml . # CodeQL for dataflow questions grep cannot answer codeql database create db --language= && codeql database analyze db --format=sarif-latest -o out.sarif # Language-specific bandit -r . -f json # Python gosec -fmt=json ./... # Go cargo audit && cargo geiger # Rust deps + unsafe surface npm audit --json # JS deps ``` Triage every tool finding through the four verification questions above. A report of unverified scanner output is worse than no report — it burns the reader's trust and buries the real bugs. ## Audit Deliverable Track coverage as you go, and state it honestly: ```markdown ## Coverage | Component | Files | Depth | Notes | |-----------|-------|------------|--------------------------------| | auth/ | 12 | Full trace | All routes traced to sinks | | billing/ | 30 | Partial | Webhook handlers only | | vendor/ | - | Excluded | Out of scope per brief | ## Findings F1. [High] Tenant isolation bypass in GET /api/reports/:id — — ``` Say what you did **not** cover. An audit that claims full coverage it did not achieve is the most damaging artifact you can produce. ## References - `references/bug-class-checklist.md` — per-language hunting checklists - `reporting-security-findings` — severity scoring and write-up format - OWASP Application Security Verification Standard (ASVS) for requirement-driven review - CWE Top 25 and the CWE hierarchy for classification