When to use this
- Any diff that adds or changes a database query, especially with dynamic filter/sort/search parameters.
- Auditing an older codebase for raw string-concatenated SQL before a security push.
- Not needed for diffs with no database access, or ones using an ORM/query builder exclusively with no raw-SQL escape hatch used.
The skill file
Copy this verbatim. It's written in the SKILL.md format (YAML frontmatter + markdown instructions) that Claude Code, and increasingly other agent tools, read directly.
--- name: sql-injection-auditor description: Trace every path from user-controlled input to a SQL query in a diff, and confirm each is parameterized/prepared rather than string-concatenated. Use for diffs that add or modify database queries, especially with dynamic filtering, sorting, or search. --- # SQL Injection Auditor ## What to trace For every SQL query in the diff (raw SQL, or a query builder's raw/unsafe escape hatch -- ORMs' normal parameterized methods are already safe and don't need this), trace backward: where does each value in the query come from? - If every value is a compile-time constant or comes from a parameterized placeholder (`?`, `$1`, named params bound separately from the query string), it's safe. - If any value is built into the query string via concatenation, f-string/template interpolation, or `.format()` -- and that value originates from user input (request params, headers, uploaded file content, even indirectly through a database value that itself came from user input) -- that's a finding, full stop, regardless of how unlikely exploitation "feels." ## Special cases that are easy to miss - Dynamic column/table names and `ORDER BY` clauses (can't be parameterized the normal way -- must be validated against an allowlist of known column/table names, never interpolated directly). - Search/filter query builders that assemble a `WHERE` clause from a list of user-supplied conditions. - Stored procedure calls with string-built arguments. ## Output For every query: safe (parameterized, cite the line) or a finding (exact concatenation point, the user-input origin, and the fix -- parameterize, or allowlist-validate for identifiers that can't be parameterized).
Installing it elsewhere
The frontmatter/body split above is Claude Code's convention. Here's how to carry the same instructions into other tools:
Save the file below verbatim (frontmatter included) at that path, project-local or in ~/.claude/skills/ for a user-level skill. Claude Code loads the name/description pair to decide when to pull it in, or you invoke it directly as /sql-injection-auditor.
Convert the YAML frontmatter to Cursor's rule format (description, globs, alwaysApply: false) and keep the markdown body as the rule content. Cursor surfaces it by description match, same idea as Claude Code's auto-load.
Codex CLI (and increasingly other agentic CLIs) read AGENTS.md at the repo root as always-on instructions. Paste the markdown body under a heading like ## {title}; for GitHub Copilot's coding agent, the equivalent file is .github/copilot-instructions.md.
Append the markdown body to .windsurfrules at the repo root. Windsurf treats the whole file as always-on context, so keep only the instructions you want applied on every request.
- Flagging an ORM's normal parameterized query method as a false positive because it "looks like" string building at a glance.
- Missing dynamic ORDER BY / column-name injection because it doesn't fit the classic WHERE-clause pattern.
- Assuming a value is safe because it "comes from the database" without checking whether that database value originated from user input upstream.