Awesome-claude-code check-sql-injection
Analyzes PHP code for SQL injection vulnerabilities. Detects query concatenation, ORM misuse, raw queries, dynamic identifiers, prepared statement bypasses.
install
source · Clone the upstream repo
git clone https://github.com/dykyi-roman/awesome-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/dykyi-roman/awesome-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/check-sql-injection" ~/.claude/skills/dykyi-roman-awesome-claude-code-check-sql-injection && rm -rf "$T"
manifest:
skills/check-sql-injection/SKILL.mdsource content
SQL Injection Security Check
Analyze PHP code for SQL injection vulnerabilities.
Detection Patterns
1. String Concatenation in Queries
// CRITICAL: Direct concatenation $sql = "SELECT * FROM users WHERE id = " . $id; $sql = "SELECT * FROM users WHERE email = '" . $email . "'"; $sql = "DELETE FROM posts WHERE id = $id"; // CRITICAL: In method public function findByEmail(string $email): ?User { $sql = "SELECT * FROM users WHERE email = '$email'"; return $this->query($sql); }
2. Variable Interpolation
// CRITICAL: Double-quoted strings $sql = "SELECT * FROM $table WHERE $column = '$value'"; $sql = "SELECT * FROM users WHERE name LIKE '%{$search}%'"; // CRITICAL: Heredoc/Nowdoc $sql = <<<SQL SELECT * FROM users WHERE id = $id SQL;
3. Dynamic Table/Column Names
// CRITICAL: User-controlled identifiers $table = $_GET['table']; $query = "SELECT * FROM $table"; $column = $request->get('sort'); $query = "SELECT * FROM users ORDER BY $column"; // Even with prepared statements: $stmt = $pdo->prepare("SELECT * FROM $table WHERE id = ?"); // Table name is still injectable!
4. ORM Raw Queries
// CRITICAL: Doctrine raw DQL $dql = "SELECT u FROM User u WHERE u.email = '$email'"; $query = $em->createQuery($dql); // CRITICAL: Query builder with raw $qb->where("u.status = $status"); $qb->orderBy($userInput); // CRITICAL: Native query $sql = "SELECT * FROM users WHERE name = '$name'"; $em->getConnection()->executeQuery($sql);
5. Eloquent/Laravel Raw
// CRITICAL: Raw methods User::whereRaw("email = '$email'")->get(); DB::select("SELECT * FROM users WHERE id = $id"); DB::raw("COUNT(*) as count WHERE status = '$status'"); // VULNERABLE: Order by User::orderBy($request->input('sort'))->get(); User::orderByRaw($request->input('order'))->get();
6. LIKE Clause Injection
// CRITICAL: Unescaped in LIKE $sql = "SELECT * FROM products WHERE name LIKE '%$search%'"; // VULNERABLE: Wildcards not escaped $stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?"); $stmt->execute(["%$search%"]); // User can input: %' OR '1'='1 // CORRECT: Escape wildcards $search = addcslashes($search, '%_'); $stmt->execute(["%{$search}%"]);
7. IN Clause Vulnerabilities
// CRITICAL: Building IN unsafely $ids = implode(',', $_GET['ids']); $sql = "SELECT * FROM users WHERE id IN ($ids)"; // VULNERABLE: Array could contain non-integers $ids = array_map('intval', $_GET['ids']); $sql = "SELECT * FROM users WHERE id IN (" . implode(',', $ids) . ")"; // Empty array results in "IN ()" - SQL error // CORRECT: Use parameter binding $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)"); $stmt->execute($ids);
8. Second-Order Injection
// CRITICAL: Data from database used unsafely $user = $this->getUserFromDatabase($id); $sql = "SELECT * FROM orders WHERE user_name = '" . $user->name . "'"; // If name was originally injected: O'Malley' OR '1'='1
9. Prepared Statement Bypasses
// VULNERABLE: prepare() but not using parameters $sql = "SELECT * FROM users WHERE id = $id"; $stmt = $pdo->prepare($sql); $stmt->execute(); // No binding! // VULNERABLE: Mixing bound and unbound $stmt = $pdo->prepare("SELECT * FROM $table WHERE id = ?"); $stmt->execute([$id]); // Table still injectable
10. Stored Procedure Injection
// CRITICAL: Procedure call with user input $sql = "CALL process_order($orderId, '$status')"; $pdo->query($sql); // CRITICAL: Even with prepare $stmt = $pdo->prepare("CALL get_user_data('$userId')"); // Parameter inside string literal is not bound
Grep Patterns
# Variable in SQL Grep: '(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\$\w+' -i --glob "**/*.php" # Concatenation in query methods Grep: "(query|execute|prepare)\s*\([^)]*\.\s*\\\$" --glob "**/*.php" # Raw ORM methods Grep: "(whereRaw|orderByRaw|selectRaw|DB::raw)\s*\(" --glob "**/*.php" # DQL with variable Grep: "createQuery\s*\([^)]*\\\$" --glob "**/*.php" # LIKE without escaping Grep: "LIKE.*'%.*\\\$.*%'" --glob "**/*.php"
Severity Classification
| Pattern | Severity |
|---|---|
| Direct concatenation in SQL | 🔴 Critical |
| $_GET/$_POST in query | 🔴 Critical |
| Dynamic table/column name | 🔴 Critical |
| ORM raw with variable | 🔴 Critical |
| LIKE without wildcard escape | 🟠 Major |
| Second-order injection risk | 🟠 Major |
Secure Patterns
PDO Prepared Statements
// Positional parameters $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ? AND status = ?'); $stmt->execute([$id, $status]); // Named parameters $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email'); $stmt->execute(['email' => $email]);
Doctrine Query Builder
$qb = $em->createQueryBuilder() ->select('u') ->from(User::class, 'u') ->where('u.email = :email') ->setParameter('email', $email);
Safe Dynamic Identifiers
// Whitelist for table/column names $allowedTables = ['users', 'orders', 'products']; if (!in_array($table, $allowedTables, true)) { throw new InvalidArgumentException('Invalid table'); } $allowedColumns = ['name', 'email', 'created_at']; $column = in_array($sort, $allowedColumns, true) ? $sort : 'id';
Laravel Eloquent
// Safe binding User::where('email', $email)->first(); // With array User::whereIn('id', $ids)->get(); // Order by validation $allowed = ['name', 'created_at']; $sort = in_array($input, $allowed) ? $input : 'id'; User::orderBy($sort)->get();
Output Format
### SQL Injection: [Description] **Severity:** 🔴 Critical **Location:** `file.php:line` **CWE:** CWE-89 (SQL Injection) **Issue:** User input is included in SQL without parameterization. **Attack Vector:** Input: `' OR '1'='1' --` Result: Query returns all rows, bypassing conditions. **Code:** ```php // Vulnerable code
Fix:
// Parameterized query
## When This Is Acceptable - **ORM query builders** — Doctrine/Eloquent query builders use parameter binding internally; they are safe by design - **Named parameters in DQL/HQL** — `:paramName` syntax with `setParameter()` is not vulnerable - **Schema migrations** — DDL statements in migrations don't process user input ### False Positive Indicators - Code uses `$qb->where('field = :value')->setParameter('value', $input)` - Code uses Doctrine DQL with bound parameters - SQL is in a migration file with no user input