check-sql-injection
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
More from dykyi-roman/awesome-claude-code
psr-overview-knowledge
PHP Standards Recommendations (PSR) overview knowledge base. Provides comprehensive reference for all accepted PSRs including PSR-1,3,4,6,7,11,12,13,14,15,16,17,18,20. Use for PSR selection decisions and compliance audits.
22detect-code-smells
Detects code smells in PHP codebases. Identifies God Class, Feature Envy, Data Clumps, Long Parameter List, Long Method, Primitive Obsession, Message Chains, Inappropriate Intimacy. Generates actionable reports with refactoring recommendations.
15clean-arch-knowledge
Clean Architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Clean Architecture and Hexagonal Architecture audits.
15ddd-knowledge
DDD architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Domain-Driven Design audits.
14testing-knowledge
Testing knowledge base for PHP 8.4 projects. Provides testing pyramid, AAA pattern, naming conventions, isolation principles, DDD testing guidelines, and PHPUnit patterns.
12bug-root-cause-finder
Root cause analysis methods for PHP bugs. Provides 5 Whys technique, fault tree analysis, git bisect guidance, and stack trace parsing.
12