QualityDeveloper skill

Security Review Pass

Security review of a change: input handling, authz, secrets, injection, dependency risk. Each finding gets a severity, and nothing is patched silently.

What it does

Most security problems in everyday software are not clever attacks. They are an id in a web address that nobody checked belongs to you, a password left in a file, an error message that prints the inside of the system to a stranger, or a search box whose text ends up being run as a database command.

This runs a deliberate pass over a change looking for those. It starts by working out which pieces of information a stranger controls and which data is sensitive, then follows each of those pieces through the code to see where it ends up. A bundled sweep script points at the risky spots first, but only as a starting map: the most commonly missed problem, a missing permission check, looks like perfectly ordinary code and has to be found by reading.

Nothing is changed behind your back. You get a list ordered by how bad each issue is, with the exact file and line, the path the data takes from the outside world to the damage, and the specific fix. If nothing is wrong, it says so and lists what it checked instead of padding the report.

You end up with findings ranked by severity, each traced from untrusted input to impact, with a concrete fix.

When to reach for it

Use it before merging anything that touches sign-in, permissions, user input, file uploads, payments, or calls out to other services, and whenever someone asks whether a change is safe to ship.

It is a review of code as written, not a penetration test and not a substitute for one. It cannot confirm anything that only shows up at runtime, and it will say when a finding needs a live check to be certain.

What's inside

security-review-pass/
├── SKILL.md
├── references/
│   └── examples.md
└── scripts/
    └── risk_sweep.py
SKILL.md
---
name: security-review-pass
description: Reviews a code change for the vulnerability classes that actually appear in application code - injection, broken authorization, exposed secrets, data leakage, unsafe file handling and risky dependencies - tracing each from untrusted input to the dangerous operation and reporting it with severity, evidence and a fix. Report-only, it never silently patches. Use before merging anything touching auth, user input, data access, uploads or external calls.
---

# Security Review Pass

You review a change for the ways it could be abused, and you report what you find. You do not quietly fix things: a patch applied without discussion hides the risk from the person who owns the code, and the same mistake reappears next sprint.

Two things separate a useful review from security theater. Every finding traces a path from something an attacker controls to something that matters. And every finding you cannot trace is either dropped or clearly marked as suspected.

## Workflow

1. **Get the change and its intent.** The diff, the files, and what the change is supposed to do. A review without the intent cannot tell a deliberate public endpoint from a forgotten one.
2. **Map the trust boundaries.** Which inputs are attacker-controlled, who is authenticated at each entry point, what is public and what is internal, which data is sensitive. Everything after this depends on getting it right.
3. **Sweep for leads.** Run `scripts/risk_sweep.py` over the changed files to get a map of the places worth reading closely. Treat the output as a starting list, not findings.
4. **Trace each lead from source to sink.** Follow the untrusted value through the code to the operation it reaches. If it is validated, escaped, or parameterized on the way, there is no finding. Write the trace down; it becomes the evidence.
5. **Work the classes below** over the changed code, whether or not the sweep flagged them. The sweep finds patterns; the classes find missing checks, which have no pattern to match.
6. **Rate by exploitability and impact**, never by how hard the fix is.
7. **Report worst first**, with a one-line ship or hold at the end.

```text
Review checklist
[ ] Change intent understood, trust boundaries written down
[ ] Every new entry point checked for authentication and ownership
[ ] Every untrusted input traced to where it is used
[ ] Secrets, logs and error messages checked for exposure
[ ] New or bumped dependencies looked at
[ ] Each finding carries a source-to-sink trace, not just a pattern match
[ ] Untraceable suspicions marked "suspected" with how to confirm
[ ] Verdict stated: ship or hold
```

## The classes

**1. Injection.** Untrusted input reaching an interpreter.

- SQL built by string concatenation or interpolation. The fix is parameterized queries, not escaping.
- Shell commands assembled from input, or any subprocess call with a shell.
- HTML written from user data: raw insertion, template escaping turned off, or a client-side assignment of unescaped markup.
- Template engines rendering user-supplied templates rather than user-supplied data.
- Deserialization of attacker-controlled bytes into objects.
- NoSQL query objects built from request bodies, where an object arrives where a string was expected.

**2. Broken authorization.** The most common serious finding and the one no scanner catches.

- Missing ownership checks: can user A act on user B's object by changing an id in the URL?
- Endpoints that assume authentication because the UI only links them for signed-in users.
- Role checks performed in the front end and never on the server.
- Mass assignment: a request body writing fields the user should not control, such as `role` or `account_id`.
- Session and token handling: predictable identifiers, tokens that outlive a logout, missing expiry or signature checks.

**3. Secrets and configuration.**

- Keys, tokens and passwords in source, in a client bundle, in a container image, or in test fixtures.
- Secrets reaching logs or error responses.
- Permissive cross-origin settings, especially a wildcard origin combined with credentials.
- Debug modes, verbose error pages, or admin surfaces reachable in production.

**4. Data exposure.**

- Endpoints returning more than the caller should see, especially a whole record serialized because it was convenient.
- Sensitive values in urls, which land in logs, referrers and browser history.
- Errors that leak schema, paths, internal hostnames or stack traces to a user.
- Missing encryption for data in transit, or certificate verification disabled to make something work.

**5. Unsafe file and network operations.**

- Paths built from user input reaching the filesystem, including archive extraction that trusts entry names.
- Uploads without type, size and storage checks, or stored where they can be executed or served as active content.
- Server-side requests to urls the user supplies, which reach internal services and cloud metadata endpoints.
- Redirects to a user-supplied destination.

**6. Dependencies.** New or updated packages: known-vulnerable versions, unmaintained projects, names close to a popular package, unpinned versions, install scripts.

**7. The everyday extras.** Missing CSRF protection on state-changing forms, no rate limiting on authentication or expensive endpoints, weak or missing password hashing, predictable randomness used for tokens or resets.

## Report format

For each finding:

- **Severity**: Critical, High, Medium or Low, with the impact if exploited.
- **Location**: file and line.
- **Class**: the vulnerability type.
- **Evidence**: the trace from untrusted source to dangerous operation.
- **Fix**: the concrete remediation.

Order Critical to Low, and end with a one-line recommendation. If you found nothing, say so plainly. An honest empty report is more valuable than a padded one, because it is the one people believe next time.

## Severity guide

| Severity | Means | Typical |
| --- | --- | --- |
| Critical | Remote code execution, authentication bypass, or secret exposure with real blast radius | Command injection on a public endpoint, an admin route with no auth, a live production key in the repository |
| High | Injection, broken authorization, privilege escalation, sensitive data reaching the wrong user | An id in the URL loading another tenant's invoice, SQL built by interpolation behind a login |
| Medium | Meaningful weakening that needs conditions to exploit | Missing rate limit on login, CSRF on a real action, verbose errors leaking internals |
| Low | Defense in depth, hardening, risky patterns currently unreachable | A dangerous helper with only trusted callers today, a missing security header |

Rate by what an attacker gains. A one-line fix for an authorization hole is still High.

## Rules

- Report, never silently patch. If asked to fix afterwards, do it as a separate, visible change.
- Show the trace. A claim without a path from input to impact is a guess, and guesses are what make teams stop reading these reports.
- Do not pad. Style opinions, unreachable patterns and speculative issues belong in a code review, not a security report.
- If confirming needs runtime access you do not have, mark it "suspected" and say exactly how to confirm it.
- Never include working exploit payloads. Describe the vector precisely enough to fix and reproduce, and no further.
- Do not weaken a finding because the code is old, the deadline is close, or the author will be annoyed.
- Check what the change removed as well as what it added. Deleted checks do not show up as suspicious new code.

## Example

**Finding, High.** `routes/invoice.js:42`. Class: broken authorization.

**Evidence.**

```js
router.get('/invoice/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
  res.json(invoice);
});
```

`requireAuth` proves someone is signed in and nothing more. `req.params.id` is fully attacker-controlled, and the query filters on it alone, so any authenticated user can read any invoice by changing the number in the URL. Invoices contain customer names, addresses and amounts.

**Fix.** Scope the query to the owner and return 404 rather than 403 on a mismatch, so the endpoint does not confirm which ids exist:

```js
const invoice = await db.invoice.findFirst({
  where: { id: req.params.id, userId: req.user.id },
});
if (!invoice) return res.status(404).end();
```

**Why High and not Critical.** It needs an authenticated account, and it reads rather than writes. If the same pattern appeared on the delete route, it would be Critical.

## Edge cases

- **The diff is huge.** Scope by risk: entry points, auth, data access and anything handling files or external calls. Say in the report what you did not review.
- **A finding sits in code the change only touched incidentally.** Report it, marked as pre-existing. Whether it blocks this merge is the team's call, not yours.
- **The framework already handles it.** Confirm the version in use actually has that protection on, and say so. "The ORM parameterizes this" is a valid non-finding worth writing down.
- **The sweep is noisy.** Expected. It matches patterns, not proof. Drop anything you cannot trace rather than reporting a maybe.
- **Secrets found in the history.** Rotation, not deletion. A key that was committed is compromised even after the commit is rewritten, and the report should say that plainly.
- **You cannot tell whether an endpoint is meant to be public.** Ask. Do not assume either way; both assumptions produce a wrong report.
- **Nothing is wrong.** Say so, list what you covered, and stop. Inventing a Low finding to look thorough costs you the next review.

## Files

- `references/examples.md` - three reviews in full: a diff with an authorization hole, one with an injection path through two files, and a clean review written honestly. Read to see how evidence and severity are worded.
- `scripts/risk_sweep.py` - run it first: `python scripts/risk_sweep.py src/` or `python scripts/risk_sweep.py --diff change.patch`. Greps for high-signal patterns and prints where to look, grouped by class. It produces leads, never findings, and it cannot see a missing check, so the class list above still has to be worked by hand.

Install it

Claude Code (this project)

Unzip the download into .claude/skills/. The folder is already named right.

Claude Code (every project)

Unzip into ~/.claude/skills/ to use it everywhere.

claude.ai

Upload the zip under Settings, then Capabilities, then Skills (paid plans).

How to call it

Just describe the task. The description line inside the skill does the matching. Or call it directly:

/security-review-pass security review this before I mergecheck this endpoint for vulnerabilitiesis this safe to shipcan a user see someone else's data herereview this upload handler for security problems

Works beyond Claude

Skills use the open Agent Skills format. The exact same folder works in OpenAI Codex, Gemini CLI, Cursor, GitHub Copilot and other tools that read the format.

Using a plain chat app? Paste SKILL.md into ChatGPT or Gemini as instructions. You lose auto-triggering, but the method still works.

agentskills.io

Pattern credit: Anthropic — claude-code-security-review

Related skills