@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/README.md +8 -0
  3. package/commands/pentest.md +5 -0
  4. package/package.json +8 -3
  5. package/skills/analyzing-tls-config/SKILL.md +221 -0
  6. package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
  7. package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
  8. package/skills/analyzing-tls-config/references/THEORY.md +128 -0
  9. package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
  10. package/skills/auditing-cors-policy/SKILL.md +186 -0
  11. package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
  12. package/skills/auditing-cors-policy/references/THEORY.md +142 -0
  13. package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
  14. package/skills/auditing-npm-dependencies/SKILL.md +254 -0
  15. package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
  16. package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
  17. package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
  18. package/skills/auditing-python-dependencies/SKILL.md +251 -0
  19. package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
  20. package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
  21. package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
  22. package/skills/checking-http-security-headers/SKILL.md +176 -0
  23. package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
  24. package/skills/checking-http-security-headers/references/THEORY.md +137 -0
  25. package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
  26. package/skills/checking-license-compliance/SKILL.md +225 -0
  27. package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
  28. package/skills/checking-license-compliance/references/THEORY.md +152 -0
  29. package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
  30. package/skills/composing-vulnerability-report/SKILL.md +212 -0
  31. package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
  32. package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
  33. package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
  34. package/skills/confirming-pentest-authorization/SKILL.md +247 -0
  35. package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
  36. package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
  37. package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
  38. package/skills/defining-pentest-scope/SKILL.md +227 -0
  39. package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
  40. package/skills/defining-pentest-scope/references/THEORY.md +170 -0
  41. package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
  42. package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
  43. package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
  44. package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
  45. package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
  46. package/skills/detecting-debug-endpoints/SKILL.md +207 -0
  47. package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
  48. package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
  49. package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
  50. package/skills/detecting-directory-listing/SKILL.md +206 -0
  51. package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
  52. package/skills/detecting-directory-listing/references/THEORY.md +203 -0
  53. package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
  54. package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
  55. package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
  56. package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
  57. package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
  58. package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
  59. package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
  60. package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
  61. package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
  62. package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
  63. package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
  64. package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
  65. package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
  66. package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
  67. package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
  68. package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
  69. package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
  70. package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
  71. package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
  72. package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
  73. package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
  74. package/skills/detecting-weak-cryptography/SKILL.md +147 -0
  75. package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
  76. package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
  77. package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
  78. package/skills/fingerprinting-server-software/SKILL.md +191 -0
  79. package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
  80. package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
  81. package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
  82. package/skills/generating-executive-summary/SKILL.md +261 -0
  83. package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
  84. package/skills/generating-executive-summary/references/THEORY.md +195 -0
  85. package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
  86. package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
  87. package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
  88. package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
  89. package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
  90. package/skills/performing-penetration-testing/SKILL.md +282 -190
  91. package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
  92. package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
  93. package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
  94. package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
  95. package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
  96. package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
  97. package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
  98. package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
  99. package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
  100. package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
  101. package/skills/recording-pentest-engagement/SKILL.md +253 -0
  102. package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
  103. package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
  104. package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
  105. package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
  106. package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
  107. package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
  108. package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
  109. package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
  110. package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
  111. package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
  112. package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
@@ -0,0 +1,317 @@
1
+ # SQL-Injection Remediation Playbook
2
+
3
+ Each section is a copy-paste-ready before/after pair for the matched
4
+ pattern. The principle is universal: parameterized query with
5
+ placeholder, values passed as a separate argument.
6
+
7
+ ## Python — sqlite3 / psycopg / SQLAlchemy
8
+
9
+ ### Before
10
+
11
+ ```python
12
+ def get_user(user_id):
13
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
14
+ return cursor.fetchone()
15
+ ```
16
+
17
+ ### After (psycopg)
18
+
19
+ ```python
20
+ def get_user(user_id):
21
+ cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
22
+ return cursor.fetchone()
23
+ ```
24
+
25
+ ### After (SQLAlchemy ORM)
26
+
27
+ ```python
28
+ def get_user(user_id):
29
+ return session.query(User).filter(User.id == user_id).first()
30
+ ```
31
+
32
+ ### Identifier interpolation (dynamic table / column) — allow-list
33
+
34
+ ```python
35
+ SORTABLE = {"id", "name", "email", "created_at"}
36
+
37
+ def list_users(sort_by):
38
+ if sort_by not in SORTABLE:
39
+ raise ValueError(f"Invalid sort column: {sort_by}")
40
+ # Now safe: sort_by is one of a finite known-safe set
41
+ cursor.execute(f"SELECT * FROM users ORDER BY {sort_by}")
42
+ return cursor.fetchall()
43
+ ```
44
+
45
+ For runtime-dynamic identifiers (rare), use psycopg's typed
46
+ identifier helper:
47
+
48
+ ```python
49
+ from psycopg.sql import SQL, Identifier
50
+
51
+ def query_table(table_name):
52
+ if table_name not in ALLOWED_TABLES:
53
+ raise ValueError()
54
+ stmt = SQL("SELECT * FROM {}").format(Identifier(table_name))
55
+ cursor.execute(stmt)
56
+ ```
57
+
58
+ ## Node.js — mysql2 / pg / sequelize / knex
59
+
60
+ ### Before (mysql2)
61
+
62
+ ```javascript
63
+ const result = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
64
+ ```
65
+
66
+ ### After (mysql2)
67
+
68
+ ```javascript
69
+ const [rows] = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
70
+ ```
71
+
72
+ ### After (pg)
73
+
74
+ ```javascript
75
+ const result = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
76
+ ```
77
+
78
+ ### Sequelize before
79
+
80
+ ```javascript
81
+ const users = await sequelize.query(
82
+ `SELECT * FROM users WHERE name = '${name}'`,
83
+ { type: QueryTypes.SELECT }
84
+ );
85
+ ```
86
+
87
+ ### Sequelize after
88
+
89
+ ```javascript
90
+ const users = await sequelize.query(
91
+ "SELECT * FROM users WHERE name = :name",
92
+ {
93
+ replacements: { name },
94
+ type: QueryTypes.SELECT
95
+ }
96
+ );
97
+ ```
98
+
99
+ ### Knex before
100
+
101
+ ```javascript
102
+ const rows = await knex.raw(`SELECT * FROM users WHERE id = ${id}`);
103
+ ```
104
+
105
+ ### Knex after (use the query builder, not raw)
106
+
107
+ ```javascript
108
+ const rows = await knex("users").where("id", id);
109
+ // Or with raw + binding:
110
+ const rows = await knex.raw("SELECT * FROM users WHERE id = ?", [id]);
111
+ ```
112
+
113
+ ## Ruby on Rails
114
+
115
+ ### Before
116
+
117
+ ```ruby
118
+ User.where("name = '#{params[:name]}'")
119
+ ```
120
+
121
+ ### After (hash form)
122
+
123
+ ```ruby
124
+ User.where(name: params[:name])
125
+ ```
126
+
127
+ ### After (array form with `?`)
128
+
129
+ ```ruby
130
+ User.where("name = ?", params[:name])
131
+ ```
132
+
133
+ ### For raw SQL
134
+
135
+ ```ruby
136
+ User.find_by_sql([
137
+ "SELECT * FROM users WHERE name = ? AND active = ?",
138
+ params[:name], true
139
+ ])
140
+ ```
141
+
142
+ ## Go (database/sql)
143
+
144
+ ### Before
145
+
146
+ ```go
147
+ rows, err := db.Query(fmt.Sprintf("SELECT * FROM users WHERE id = %d", id))
148
+ ```
149
+
150
+ ### After (MySQL / SQLite — `?` placeholders)
151
+
152
+ ```go
153
+ rows, err := db.Query("SELECT * FROM users WHERE id = ?", id)
154
+ ```
155
+
156
+ ### After (PostgreSQL — `$1` placeholders)
157
+
158
+ ```go
159
+ rows, err := db.Query("SELECT * FROM users WHERE id = $1", id)
160
+ ```
161
+
162
+ ### sqlx (named binds)
163
+
164
+ ```go
165
+ type User struct {
166
+ ID int `db:"id"`
167
+ Name string `db:"name"`
168
+ }
169
+ var u User
170
+ err := db.Get(&u, "SELECT * FROM users WHERE id = $1", id)
171
+ ```
172
+
173
+ ## Java (JDBC)
174
+
175
+ ### Before
176
+
177
+ ```java
178
+ Statement stmt = conn.createStatement();
179
+ ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE id = " + id);
180
+ ```
181
+
182
+ ### After
183
+
184
+ ```java
185
+ PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
186
+ stmt.setInt(1, id);
187
+ ResultSet rs = stmt.executeQuery();
188
+ ```
189
+
190
+ ### Hibernate / JPA
191
+
192
+ ```java
193
+ List<User> users = entityManager
194
+ .createQuery("SELECT u FROM User u WHERE u.id = :id", User.class)
195
+ .setParameter("id", id)
196
+ .getResultList();
197
+ ```
198
+
199
+ ## PHP
200
+
201
+ ### Before (legacy mysql_query)
202
+
203
+ ```php
204
+ $result = mysql_query("SELECT * FROM users WHERE id = " . $id);
205
+ ```
206
+
207
+ ### After (PDO with named placeholders)
208
+
209
+ ```php
210
+ $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
211
+ $stmt->execute(["id" => $id]);
212
+ $user = $stmt->fetch();
213
+ ```
214
+
215
+ ### After (mysqli with positional)
216
+
217
+ ```php
218
+ $stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
219
+ $stmt->bind_param("i", $id);
220
+ $stmt->execute();
221
+ $result = $stmt->get_result();
222
+ ```
223
+
224
+ ## C# / .NET
225
+
226
+ ### Before
227
+
228
+ ```csharp
229
+ var cmd = new SqlCommand("SELECT * FROM users WHERE id = " + userId, conn);
230
+ ```
231
+
232
+ ### After
233
+
234
+ ```csharp
235
+ var cmd = new SqlCommand("SELECT * FROM users WHERE id = @userId", conn);
236
+ cmd.Parameters.AddWithValue("@userId", userId);
237
+ ```
238
+
239
+ ### Entity Framework / EF Core
240
+
241
+ ```csharp
242
+ var user = context.Users.FirstOrDefault(u => u.Id == userId);
243
+ // or with raw SQL + interpolated query (EF Core auto-parameterizes interpolation in FromSqlInterpolated):
244
+ var users = context.Users.FromSqlInterpolated(
245
+ $"SELECT * FROM users WHERE id = {userId}"
246
+ ).ToList();
247
+ ```
248
+
249
+ Note: `FromSqlInterpolated` is one of the rare safe interpolation
250
+ APIs — EF Core treats interpolated values as parameters under the
251
+ hood.
252
+
253
+ ## Pre-commit hook
254
+
255
+ `.pre-commit-config.yaml`:
256
+
257
+ ```yaml
258
+ repos:
259
+ - repo: local
260
+ hooks:
261
+ - id: scan-sqli
262
+ name: Scan for SQL-injection patterns
263
+ entry: python3 plugins/security/penetration-tester/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py
264
+ language: system
265
+ args: ['--min-severity', 'high']
266
+ pass_filenames: false
267
+ ```
268
+
269
+ ## CI integration
270
+
271
+ ```yaml
272
+ - name: SQL-injection pattern scan
273
+ run: |
274
+ python3 plugins/security/penetration-tester/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py \
275
+ . --min-severity high --format json --output sqli-scan.json
276
+ - run: |
277
+ if jq 'length > 0' sqli-scan.json | grep -q true; then
278
+ echo "::error::SQL injection patterns detected"
279
+ cat sqli-scan.json
280
+ exit 1
281
+ fi
282
+ ```
283
+
284
+ ## After remediation: dynamic-SQL hardening
285
+
286
+ If your application legitimately builds dynamic SQL (multi-tenant
287
+ routing, user-defined queries in a reporting tool), the
288
+ parameterization-only approach isn't sufficient. Add:
289
+
290
+ 1. **Allow-list identifiers.** Maintain a fixed set of permitted
291
+ table / column names. Validate against the set before
292
+ interpolation.
293
+
294
+ 2. **Use the database's identifier-quoting API.** Each driver
295
+ exposes one (psycopg's `Identifier`, Sequelize's
296
+ `sequelize.escape`, etc.).
297
+
298
+ 3. **Lowest-privilege database role.** The application's DB user
299
+ should only have the permissions the app needs. A SELECT-only
300
+ query running under a role that can't INSERT/UPDATE/DELETE
301
+ bounds the blast radius of any injection that does slip through.
302
+
303
+ 4. **Database firewall (optional, high-value targets).** Tools
304
+ like ProxySQL or PostgreSQL's `pg_audit` can log every query
305
+ and alert on anomalies (queries containing `; DROP`, queries
306
+ accessing unexpected tables).
307
+
308
+ ## Verification after remediation
309
+
310
+ ```bash
311
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py \
312
+ /path/to/repo --min-severity medium
313
+ ```
314
+
315
+ Expected: exit 0, zero medium-or-higher findings. Remaining LOW
316
+ findings (Django `.extra()` calls with verified allow-listed
317
+ identifiers) are acceptable trade-offs documented per-call.
@@ -0,0 +1,261 @@
1
+ # SQL-Injection Pattern Theory
2
+
3
+ ## Why this class persists
4
+
5
+ SQL injection has been the canonical web vulnerability since the
6
+ late 1990s. Every framework's documentation warns about it. Every
7
+ ORM defaults to parameterized queries. Yet it remains a top-10
8
+ finding category year over year.
9
+
10
+ Three patterns generate ongoing introductions:
11
+
12
+ 1. **The "just need a dynamic table name" trap.** ORMs handle
13
+ parameterized VALUES well but typically can't parameterize
14
+ IDENTIFIERS (table names, column names). An engineer
15
+ string-builds the identifier portion, leaves the values
16
+ parameterized, and convinces themselves it's safe. Then a
17
+ feature request adds user-controlled column sorting and the
18
+ "safe" pattern becomes the vulnerability.
19
+
20
+ 2. **Legacy code that predates the ORM.** Pre-ORM Java / PHP /
21
+ classic ASP codebases used raw JDBC / mysql_query() / Recordset
22
+ APIs. Migrating to parameterized queries is mechanical but
23
+ tedious. Many migrations stalled at 70-80% and the remaining
24
+ raw queries are still in production.
25
+
26
+ 3. **Misuse of "raw" escape hatches.** Every ORM has a raw()
27
+ method (knex.raw, sequelize.query, ActiveRecord.execute,
28
+ Django .extra()) for cases where the ORM's query builder
29
+ doesn't cover something. The escape hatch is fine; concat
30
+ into the escape hatch is the failure mode.
31
+
32
+ ## How parameterization actually works
33
+
34
+ The vulnerable pattern looks like this (Python):
35
+
36
+ ```python
37
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
38
+ ```
39
+
40
+ The string is built by Python before being passed to the database
41
+ driver. The driver sees one giant string `SELECT * FROM users
42
+ WHERE id = 1 OR 1=1`. The injection is in the string before the
43
+ SQL parser even sees it. The parser parses the whole thing as a
44
+ single statement.
45
+
46
+ Parameterized version:
47
+
48
+ ```python
49
+ cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
50
+ ```
51
+
52
+ The driver sends the query (with `%s` as a placeholder) AND the
53
+ parameters AS SEPARATE PROTOCOL FIELDS. The database parses the
54
+ query first, identifies the placeholder, then BINDS the parameter
55
+ into the parameter slot. The bound parameter is not parsed as
56
+ SQL — it's treated as a literal value, regardless of what
57
+ characters it contains.
58
+
59
+ Even an attacker sending `1 OR 1=1` as the `user_id` value gets
60
+ treated as the literal string `1 OR 1=1` looking for a row where
61
+ the ID column equals that exact string.
62
+
63
+ This is why parameterization works: the parser separates structure
64
+ from data. Concatenation eliminates the separation.
65
+
66
+ ## Per-language patterns
67
+
68
+ ### Python
69
+
70
+ **Unsafe:**
71
+
72
+ ```python
73
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
74
+ cursor.execute("SELECT * FROM users WHERE id = " + str(user_id))
75
+ cursor.execute("SELECT * FROM users WHERE id = %s" % user_id)
76
+ cursor.execute("SELECT * FROM users WHERE id = {}".format(user_id))
77
+ ```
78
+
79
+ **Safe:**
80
+
81
+ ```python
82
+ # sqlite3
83
+ cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
84
+
85
+ # psycopg / psycopg2
86
+ cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
87
+
88
+ # SQLAlchemy Core
89
+ result = conn.execute(text("SELECT * FROM users WHERE id = :uid"), {"uid": user_id})
90
+
91
+ # SQLAlchemy ORM
92
+ session.query(User).filter(User.id == user_id).all()
93
+ ```
94
+
95
+ ### Node.js
96
+
97
+ **Unsafe:**
98
+
99
+ ```javascript
100
+ db.query(`SELECT * FROM users WHERE id = ${userId}`)
101
+ db.query("SELECT * FROM users WHERE id = " + userId)
102
+ sequelize.query(`SELECT * FROM users WHERE name = '${name}'`)
103
+ ```
104
+
105
+ **Safe:**
106
+
107
+ ```javascript
108
+ // mysql2
109
+ db.query("SELECT * FROM users WHERE id = ?", [userId])
110
+
111
+ // pg
112
+ db.query("SELECT * FROM users WHERE id = $1", [userId])
113
+
114
+ // sequelize with replacements
115
+ sequelize.query(
116
+ "SELECT * FROM users WHERE name = :name",
117
+ { replacements: { name }, type: QueryTypes.SELECT }
118
+ )
119
+
120
+ // knex (parameterized by default)
121
+ knex("users").where("id", userId)
122
+ ```
123
+
124
+ ### Ruby (Rails)
125
+
126
+ **Unsafe:**
127
+
128
+ ```ruby
129
+ User.where("name = '#{name}'")
130
+ ActiveRecord::Base.connection.execute("SELECT * FROM users WHERE id = #{id}")
131
+ ```
132
+
133
+ **Safe:**
134
+
135
+ ```ruby
136
+ # Hash conditions
137
+ User.where(name: name)
138
+
139
+ # Array conditions with ?
140
+ User.where("name = ?", name)
141
+
142
+ # Or for raw SQL:
143
+ User.find_by_sql(["SELECT * FROM users WHERE name = ?", name])
144
+ ```
145
+
146
+ ### Go
147
+
148
+ **Unsafe:**
149
+
150
+ ```go
151
+ db.Query(fmt.Sprintf("SELECT * FROM users WHERE id = %d", id))
152
+ db.Query("SELECT * FROM users WHERE id = " + strconv.Itoa(id))
153
+ ```
154
+
155
+ **Safe:**
156
+
157
+ ```go
158
+ db.Query("SELECT * FROM users WHERE id = ?", id)
159
+ // PostgreSQL driver uses $1, $2, ...:
160
+ db.Query("SELECT * FROM users WHERE id = $1", id)
161
+ ```
162
+
163
+ ### Java
164
+
165
+ **Unsafe:**
166
+
167
+ ```java
168
+ Statement stmt = conn.createStatement();
169
+ stmt.executeQuery("SELECT * FROM users WHERE id = " + id);
170
+ ```
171
+
172
+ **Safe:**
173
+
174
+ ```java
175
+ PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
176
+ stmt.setInt(1, id);
177
+ ResultSet rs = stmt.executeQuery();
178
+ ```
179
+
180
+ ### PHP
181
+
182
+ **Unsafe:**
183
+
184
+ ```php
185
+ mysqli_query($conn, "SELECT * FROM users WHERE id = $id");
186
+ mysql_query("SELECT * FROM users WHERE id = " . $id);
187
+ ```
188
+
189
+ **Safe (PDO):**
190
+
191
+ ```php
192
+ $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
193
+ $stmt->execute(["id" => $id]);
194
+ ```
195
+
196
+ **Safe (mysqli):**
197
+
198
+ ```php
199
+ $stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
200
+ $stmt->bind_param("i", $id);
201
+ $stmt->execute();
202
+ ```
203
+
204
+ ## The identifier-interpolation problem
205
+
206
+ ORMs parameterize VALUES, not IDENTIFIERS. If you need to
207
+ dynamically choose a table or column at runtime (e.g., for
208
+ multi-tenant routing or user-controlled sort columns), you can't
209
+ use parameter binding for it.
210
+
211
+ The right pattern is allow-list validation:
212
+
213
+ ```python
214
+ # Allow-list known columns
215
+ SORTABLE_COLUMNS = {"id", "name", "created_at", "updated_at"}
216
+ sort_col = request.args.get("sort_by")
217
+ if sort_col not in SORTABLE_COLUMNS:
218
+ raise ValueError("Invalid sort column")
219
+ cursor.execute(f"SELECT * FROM users ORDER BY {sort_col}", ())
220
+ ```
221
+
222
+ The interpolated `sort_col` is now constrained to a finite set of
223
+ known-safe values. The f-string is fine because the only possible
224
+ values are pre-validated.
225
+
226
+ For dynamic table names (rare; usually indicates a schema-design
227
+ issue), apply the same allow-list pattern PLUS quote the identifier:
228
+
229
+ ```python
230
+ # Python with psycopg
231
+ from psycopg.sql import SQL, Identifier
232
+ cursor.execute(SQL("SELECT * FROM {}").format(Identifier(table_name)))
233
+ ```
234
+
235
+ `Identifier()` applies the database's identifier quoting rules,
236
+ preventing the table-name from being parsed as SQL.
237
+
238
+ ## Why the scanner can't be perfect
239
+
240
+ Static-pattern matching has irreducible false positives:
241
+
242
+ - A literal-only f-string `f"SELECT * FROM users WHERE id = 1"`
243
+ matches the pattern but has no injection vector (the interpolated
244
+ value is `1`, a literal).
245
+ - A pre-validated value `f"SELECT ... ORDER BY {col}"` where `col`
246
+ is from a known allow-list looks identical to the vulnerable
247
+ pattern but is safe.
248
+
249
+ A full taint-tracking AST analyzer (Semgrep, CodeQL, Bandit with
250
+ `-r`) would catch these. This skill is a regex pass — high
251
+ recall, moderate precision. Treat findings as "verify each by
252
+ reading the code," not "auto-merge a fix."
253
+
254
+ ## Primary sources
255
+
256
+ - [CWE-89 SQL Injection](https://cwe.mitre.org/data/definitions/89.html)
257
+ - [OWASP A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)
258
+ - [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
259
+ - [Bobby Tables — A guide to preventing SQL injection](https://bobby-tables.com/)
260
+ - Psycopg parameterized-query docs
261
+ - ActiveRecord query interface documentation