$npx -y skills add utkusen/sast-skills --skill sast-sqliDetect SQL injection vulnerabilities in a codebase using a three-phase approach: recon (find unsafe SQL construction sites), batched verify (trace user input to those sites in parallel subagents, 3 sites each), and merge (consolidate batch results). Covers string concat, f-string
| 1 | # SQL Injection (SQLi) Detection |
| 2 | |
| 3 | You are performing a focused security assessment to find SQL injection vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: **recon** (find vulnerable SQL construction sites), **batched verify** (taint analysis in parallel batches of 3), and **merge** (consolidate batch reports into one file). |
| 4 | |
| 5 | **Prerequisites**: `sast/architecture.md` must exist. Run the analysis skill first if it doesn't. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## What is SQL Injection |
| 10 | |
| 11 | SQL injection occurs when user-supplied input is incorporated into SQL queries through string concatenation or interpolation rather than parameterized binding. This allows attackers to alter query logic, bypass authentication, extract sensitive data, modify or delete records, and in some configurations execute OS commands. |
| 12 | |
| 13 | The core pattern: *unvalidated, unparameterized user input reaches a SQL query execution call.* |
| 14 | |
| 15 | ### What SQLi IS |
| 16 | |
| 17 | - Concatenating user input directly into a SQL string: `"SELECT * FROM users WHERE name = '" + username + "'"` |
| 18 | - Using string formatting to build queries: `f"SELECT * FROM orders WHERE id = {order_id}"` |
| 19 | - Dynamic `ORDER BY` / `GROUP BY` / table/column names from user input with no allowlist validation |
| 20 | - ORM raw query methods with unsanitized input: `User.objects.raw(f"SELECT * WHERE id={id}")`, `$queryRawUnsafe(input)` |
| 21 | - Second-order injection: input is stored in the DB and later used in a raw query without re-sanitization |
| 22 | |
| 23 | ### What SQLi is NOT |
| 24 | |
| 25 | Do not flag these as SQLi: |
| 26 | |
| 27 | - **IDOR**: Changing `?id=1` to `?id=2` to access another user's data — that's Insecure Direct Object Reference, a separate class |
| 28 | - **Mass assignment**: Setting extra ORM model fields from user input — different vulnerability |
| 29 | - **XSS via database**: Storing a `<script>` tag in the DB that's later rendered unescaped — that's XSS, not SQLi |
| 30 | - **NoSQL injection**: Injecting into MongoDB operators — similar concept but a distinct vulnerability class |
| 31 | - **Safe ORM queries**: Parameterized ORM lookups like `User.objects.filter(id=user_id)` or `User.find(params[:id])` — do not flag these |
| 32 | |
| 33 | ### Patterns That Prevent SQLi |
| 34 | |
| 35 | When you see these patterns, the code is likely **not vulnerable**: |
| 36 | |
| 37 | **1. Parameterized queries / prepared statements (most common fix)** |
| 38 | ``` |
| 39 | # Python — cursor.execute with tuple binding |
| 40 | cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) |
| 41 | |
| 42 | # Node.js — mysql2 / pg placeholder binding |
| 43 | db.query("SELECT * FROM users WHERE id = ?", [userId]) |
| 44 | pool.query("SELECT * FROM users WHERE id = $1", [userId]) |
| 45 | |
| 46 | # Java — PreparedStatement |
| 47 | PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); |
| 48 | ps.setInt(1, userId); |
| 49 | |
| 50 | # Go — database/sql placeholder |
| 51 | db.QueryRow("SELECT * FROM users WHERE id = $1", userID) |
| 52 | |
| 53 | # PHP — PDO with named params |
| 54 | $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); |
| 55 | $stmt->execute(['id' => $userId]); |
| 56 | |
| 57 | # C# — SqlCommand with parameters |
| 58 | cmd.CommandText = "SELECT * FROM users WHERE id = @id"; |
| 59 | cmd.Parameters.AddWithValue("@id", userId); |
| 60 | ``` |
| 61 | |
| 62 | **2. ORM query builder (safe by default)** |
| 63 | ``` |
| 64 | # Django ORM |
| 65 | User.objects.filter(id=user_id) |
| 66 | |
| 67 | # ActiveRecord (Rails) |
| 68 | User.find(params[:id]) |
| 69 | User.where(name: params[:name]) |
| 70 | |
| 71 | # Prisma (tagged template literal form of $queryRaw) |
| 72 | await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}` |
| 73 | |
| 74 | # Laravel Eloquent (non-raw) |
| 75 | User::find($id) |
| 76 | ``` |
| 77 | |
| 78 | **3. Allowlist validation for dynamic identifiers** |
| 79 | ``` |
| 80 | # Dynamic ORDER BY — validate column name against a hardcoded set before interpolating |
| 81 | ALLOWED_COLUMNS = {'name', 'created_at', 'price'} |
| 82 | if sort_col not in ALLOWED_COLUMNS: |
| 83 | raise ValueError("Invalid column") |
| 84 | query = f"SELECT * FROM products ORDER BY {sort_col}" # safe only after allowlist check |
| 85 | ``` |
| 86 | |
| 87 | --- |
| 88 | |
| 89 | ## Vulnerable vs. Secure Examples |
| 90 | |
| 91 | ### Python — Django (raw SQL) |
| 92 | |
| 93 | ```python |
| 94 | # VULNERABLE: f-string interpolation in raw() |
| 95 | def search_users(request): |
| 96 | username = request.GET.get('username') |
| 97 | users = User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'") |
| 98 | return JsonResponse(list(users.values()), safe=False) |
| 99 | |
| 100 | # SECURE: parameterized raw() |
| 101 | def search_users(request): |
| 102 | username = request.GET.get('username') |
| 103 | users = User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username]) |
| 104 | return JsonResponse(list(users.values()), safe=False) |
| 105 | ``` |
| 106 | |
| 107 | ### Python — Flask / SQLAlchemy |
| 108 | |
| 109 | ```python |
| 110 | # VULNERABLE: f-string into text() |
| 111 | @app.route('/search') |
| 112 | def search(): |
| 113 | name = request.a |