@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.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Static-analysis scan for insecure-deserialization API usage.
|
|
3
|
+
|
|
4
|
+
References:
|
|
5
|
+
CWE-502 Deserialization of Untrusted Data
|
|
6
|
+
OWASP A08:2021 Software and Data Integrity Failures
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
17
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
19
|
+
|
|
20
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
21
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
22
|
+
|
|
23
|
+
SKILL_ID = "detecting-insecure-deserialization"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PY_PATTERNS = [
|
|
27
|
+
("Python pickle.loads", Severity.CRITICAL, r"\b(?:cPickle|pickle)\.loads?\s*\(", "python"),
|
|
28
|
+
("Python dill.loads", Severity.CRITICAL, r"\bdill\.loads?\s*\(", "python"),
|
|
29
|
+
(
|
|
30
|
+
"Python yaml.load() without Loader=Safe",
|
|
31
|
+
Severity.CRITICAL,
|
|
32
|
+
r"\byaml\.load\s*\((?![^)]*Loader\s*=\s*[Yy]aml\.(?:Safe|safe_load))",
|
|
33
|
+
"python",
|
|
34
|
+
),
|
|
35
|
+
("Python yaml.unsafe_load (explicit unsafe)", Severity.CRITICAL, r"\byaml\.unsafe_load\s*\(", "python"),
|
|
36
|
+
("Python yaml.full_load", Severity.HIGH, r"\byaml\.full_load\s*\(", "python"),
|
|
37
|
+
("Python shelve.open (pickle-backed)", Severity.HIGH, r"\bshelve\.open\s*\(", "python"),
|
|
38
|
+
("Python marshal.loads (legacy, unsafe)", Severity.CRITICAL, r"\bmarshal\.loads?\s*\(", "python"),
|
|
39
|
+
]
|
|
40
|
+
JS_PATTERNS = [
|
|
41
|
+
(
|
|
42
|
+
"Node node-serialize.unserialize (known-vulnerable lib)",
|
|
43
|
+
Severity.CRITICAL,
|
|
44
|
+
r"require\s*\(\s*['\"]node-serialize['\"]\s*\)|from\s+['\"]node-serialize['\"]",
|
|
45
|
+
"javascript",
|
|
46
|
+
),
|
|
47
|
+
("JSON.parse with reviver function", Severity.MEDIUM, r"JSON\.parse\s*\([^,)]+,\s*function\s*\(", "javascript"),
|
|
48
|
+
]
|
|
49
|
+
RUBY_PATTERNS = [
|
|
50
|
+
("Ruby Marshal.load on non-literal", Severity.CRITICAL, r"\bMarshal\.load\s*\(\s*(?!['\"])", "ruby"),
|
|
51
|
+
("Ruby YAML.load without permitted_classes", Severity.HIGH, r"\bYAML\.load\s*\((?![^)]*permitted_classes)", "ruby"),
|
|
52
|
+
]
|
|
53
|
+
JAVA_PATTERNS = [
|
|
54
|
+
("Java ObjectInputStream.readObject", Severity.CRITICAL, r"\bObjectInputStream\b[^;]*\.readObject\s*\(", "java"),
|
|
55
|
+
(
|
|
56
|
+
"Java XMLDecoder.readObject (unsafe XML deserialization)",
|
|
57
|
+
Severity.CRITICAL,
|
|
58
|
+
r"\bXMLDecoder\b[^;]*\.readObject\s*\(",
|
|
59
|
+
"java",
|
|
60
|
+
),
|
|
61
|
+
("Java SnakeYAML new Yaml() default constructor (unsafe)", Severity.HIGH, r"new\s+Yaml\s*\(\s*\)", "java"),
|
|
62
|
+
]
|
|
63
|
+
PHP_PATTERNS = [
|
|
64
|
+
("PHP unserialize on non-literal", Severity.CRITICAL, r"\bunserialize\s*\(\s*(?!['\"])", "php"),
|
|
65
|
+
]
|
|
66
|
+
CSHARP_PATTERNS = [
|
|
67
|
+
(
|
|
68
|
+
"C# BinaryFormatter.Deserialize (deprecated unsafe)",
|
|
69
|
+
Severity.CRITICAL,
|
|
70
|
+
r"\bBinaryFormatter\s*\(?\s*\)?\s*\.\s*Deserialize\s*\(",
|
|
71
|
+
"csharp",
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"C# NetDataContractSerializer.ReadObject",
|
|
75
|
+
Severity.CRITICAL,
|
|
76
|
+
r"\bNetDataContractSerializer\b[^;]*\.(?:Read|Deserialize)",
|
|
77
|
+
"csharp",
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"C# LosFormatter.Deserialize (ViewState)",
|
|
81
|
+
Severity.CRITICAL,
|
|
82
|
+
r"\bLosFormatter\b[^;]*\.Deserialize\s*\(",
|
|
83
|
+
"csharp",
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
"C# ObjectStateFormatter.Deserialize (ViewState)",
|
|
87
|
+
Severity.CRITICAL,
|
|
88
|
+
r"\bObjectStateFormatter\b[^;]*\.Deserialize\s*\(",
|
|
89
|
+
"csharp",
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
"C# JavaScriptSerializer with SimpleTypeResolver",
|
|
93
|
+
Severity.HIGH,
|
|
94
|
+
r"new\s+JavaScriptSerializer\s*\(\s*new\s+SimpleTypeResolver",
|
|
95
|
+
"csharp",
|
|
96
|
+
),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
LANG_EXT_MAP = {
|
|
101
|
+
"python": {".py"},
|
|
102
|
+
"javascript": {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"},
|
|
103
|
+
"ruby": {".rb"},
|
|
104
|
+
"java": {".java", ".kt", ".scala"},
|
|
105
|
+
"php": {".php"},
|
|
106
|
+
"csharp": {".cs"},
|
|
107
|
+
}
|
|
108
|
+
LANG_PATTERNS = {
|
|
109
|
+
"python": PY_PATTERNS,
|
|
110
|
+
"javascript": JS_PATTERNS,
|
|
111
|
+
"ruby": RUBY_PATTERNS,
|
|
112
|
+
"java": JAVA_PATTERNS,
|
|
113
|
+
"php": PHP_PATTERNS,
|
|
114
|
+
"csharp": CSHARP_PATTERNS,
|
|
115
|
+
}
|
|
116
|
+
SKIP_DIRS = {
|
|
117
|
+
"node_modules",
|
|
118
|
+
".git",
|
|
119
|
+
"dist",
|
|
120
|
+
"build",
|
|
121
|
+
"target",
|
|
122
|
+
".cache",
|
|
123
|
+
".pnpm-store",
|
|
124
|
+
".venv",
|
|
125
|
+
"venv",
|
|
126
|
+
"__pycache__",
|
|
127
|
+
".astro",
|
|
128
|
+
".next",
|
|
129
|
+
".nuxt",
|
|
130
|
+
"vendor",
|
|
131
|
+
}
|
|
132
|
+
TEST_DIRS = {"tests", "test", "__tests__", "spec", "specs"}
|
|
133
|
+
MAX_FILE_SIZE = 5 * 1024 * 1024
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def should_skip_path(path: Path, include_tests: bool) -> bool:
|
|
137
|
+
parts = set(path.parts)
|
|
138
|
+
if parts & SKIP_DIRS:
|
|
139
|
+
return True
|
|
140
|
+
if not include_tests and parts & TEST_DIRS:
|
|
141
|
+
return True
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def detect_language(path: Path, langs: set[str]) -> str | None:
|
|
146
|
+
suf = path.suffix.lower()
|
|
147
|
+
for lang in langs:
|
|
148
|
+
if suf in LANG_EXT_MAP[lang]:
|
|
149
|
+
return lang
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def scan_file(file_path: Path, repo_root: Path, langs: set[str]) -> list[Finding]:
|
|
154
|
+
findings = []
|
|
155
|
+
lang = detect_language(file_path, langs)
|
|
156
|
+
if lang is None:
|
|
157
|
+
return findings
|
|
158
|
+
try:
|
|
159
|
+
if file_path.stat().st_size > MAX_FILE_SIZE:
|
|
160
|
+
return findings
|
|
161
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
162
|
+
except (OSError, ValueError):
|
|
163
|
+
return findings
|
|
164
|
+
try:
|
|
165
|
+
rel = str(file_path.relative_to(repo_root))
|
|
166
|
+
except ValueError:
|
|
167
|
+
rel = str(file_path)
|
|
168
|
+
|
|
169
|
+
for title, sev, pattern, _lang in LANG_PATTERNS[lang]:
|
|
170
|
+
for m in re.finditer(pattern, text, re.MULTILINE):
|
|
171
|
+
line_no = text[: m.start()].count("\n") + 1
|
|
172
|
+
snippet = text.splitlines()[line_no - 1].strip()[:160]
|
|
173
|
+
findings.append(
|
|
174
|
+
Finding(
|
|
175
|
+
skill_id=SKILL_ID,
|
|
176
|
+
title=f"{title} at {rel}:{line_no}",
|
|
177
|
+
severity=sev,
|
|
178
|
+
target=f"{rel}:{line_no}",
|
|
179
|
+
detail=(
|
|
180
|
+
f"File {rel} line {line_no} calls {title}: `{snippet}`. "
|
|
181
|
+
"If the input is attacker-controllable, this is an "
|
|
182
|
+
"arbitrary-code-execution vector via deserialization "
|
|
183
|
+
"gadget chains."
|
|
184
|
+
),
|
|
185
|
+
remediation=(
|
|
186
|
+
"Migrate to a schema-validated format. Python: json + "
|
|
187
|
+
"Pydantic. Ruby: JSON with strict schema. Java: Jackson "
|
|
188
|
+
"with allow-list. PHP: json_decode. .NET: System.Text.Json. "
|
|
189
|
+
"If polymorphic types are required, use HMAC-signed "
|
|
190
|
+
"serialization with explicit type allow-list. See "
|
|
191
|
+
"references/PLAYBOOK.md."
|
|
192
|
+
),
|
|
193
|
+
cwe_id="CWE-502",
|
|
194
|
+
affected_control="OWASP A08:2021",
|
|
195
|
+
evidence=(("file", rel), ("line", line_no), ("language", lang), ("snippet", snippet)),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
return findings
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def walk_repo(root: Path, include_tests: bool, langs: set[str]) -> list[Path]:
|
|
202
|
+
out = []
|
|
203
|
+
valid_exts = set()
|
|
204
|
+
for lang in langs:
|
|
205
|
+
valid_exts |= LANG_EXT_MAP[lang]
|
|
206
|
+
for p in root.rglob("*"):
|
|
207
|
+
if not p.is_file():
|
|
208
|
+
continue
|
|
209
|
+
if should_skip_path(p, include_tests):
|
|
210
|
+
continue
|
|
211
|
+
if p.suffix.lower() not in valid_exts:
|
|
212
|
+
continue
|
|
213
|
+
out.append(p)
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main(argv: list[str] | None = None) -> int:
|
|
218
|
+
parser = argparse.ArgumentParser(description="Insecure-deserialization scanner")
|
|
219
|
+
parser.add_argument("path", type=Path)
|
|
220
|
+
parser.add_argument("--output", default=None)
|
|
221
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
222
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
223
|
+
parser.add_argument("--include-tests", action="store_true")
|
|
224
|
+
parser.add_argument("--languages", default="all")
|
|
225
|
+
args = parser.parse_args(argv)
|
|
226
|
+
|
|
227
|
+
if args.languages == "all":
|
|
228
|
+
langs = set(LANG_PATTERNS.keys())
|
|
229
|
+
else:
|
|
230
|
+
langs = {lang.strip() for lang in args.languages.split(",") if lang.strip() in LANG_PATTERNS}
|
|
231
|
+
|
|
232
|
+
root = args.path.resolve()
|
|
233
|
+
if not root.exists():
|
|
234
|
+
sys.stderr.write(f"ERROR: path does not exist: {root}\n")
|
|
235
|
+
return 2
|
|
236
|
+
|
|
237
|
+
files = walk_repo(root, args.include_tests, langs)
|
|
238
|
+
findings: list[Finding] = []
|
|
239
|
+
for f in files:
|
|
240
|
+
findings.extend(scan_file(f, root, langs))
|
|
241
|
+
|
|
242
|
+
floor = Severity(args.min_severity)
|
|
243
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
244
|
+
|
|
245
|
+
emit(findings, args.output, args.format, str(root))
|
|
246
|
+
return exit_code(findings)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: detecting-sql-injection-patterns
|
|
3
|
+
description: |
|
|
4
|
+
Scan a source tree for SQL-injection vulnerable patterns: string
|
|
5
|
+
concatenation into queries, f-string interpolation in SQL,
|
|
6
|
+
string-format substitution into raw queries, deprecated cursor
|
|
7
|
+
methods (cursor.execute with % formatting), Knex / Sequelize raw()
|
|
8
|
+
with template interpolation, sequelize.query with replacements.
|
|
9
|
+
Use when: pre-commit code review, post-feature SQL-touching
|
|
10
|
+
release, inheriting a legacy codebase that predates ORMs, or
|
|
11
|
+
post-bug-report investigation.
|
|
12
|
+
Threshold: any source line where SQL keywords (SELECT / INSERT /
|
|
13
|
+
UPDATE / DELETE / FROM / WHERE) appear in a string that's being
|
|
14
|
+
built via concatenation, f-string, %-format, or .format() with
|
|
15
|
+
variable input.
|
|
16
|
+
Trigger with: "scan for sqli", "sql injection patterns",
|
|
17
|
+
"check raw queries", "audit cursor.execute".
|
|
18
|
+
allowed-tools:
|
|
19
|
+
- Read
|
|
20
|
+
- Bash(python3:*)
|
|
21
|
+
- Glob
|
|
22
|
+
- Grep
|
|
23
|
+
disallowed-tools:
|
|
24
|
+
- Bash(rm:*)
|
|
25
|
+
- Bash(curl:*)
|
|
26
|
+
version: 3.0.0-dev
|
|
27
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
28
|
+
license: MIT
|
|
29
|
+
compatibility: Designed for Claude Code
|
|
30
|
+
tags:
|
|
31
|
+
- security
|
|
32
|
+
- static-analysis
|
|
33
|
+
- sql-injection
|
|
34
|
+
- pentest
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Detecting SQL Injection Patterns
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
SQL injection (CWE-89, OWASP A03:2021) remains one of the highest-
|
|
42
|
+
impact and most-easily-introduced vulnerability classes. The fix is
|
|
43
|
+
near-universal: use parameterized queries. The cause when introduced:
|
|
44
|
+
an engineer concatenates user input into a SQL string because the
|
|
45
|
+
ORM's parameterization mechanism wasn't obvious, or because they
|
|
46
|
+
"just need to add a quick condition."
|
|
47
|
+
|
|
48
|
+
The scanner reads source files and grades each apparent SQL-string
|
|
49
|
+
construction against the threshold table.
|
|
50
|
+
|
|
51
|
+
## When the skill produces findings
|
|
52
|
+
|
|
53
|
+
| Finding | Severity | Threshold | Affected control |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| f-string with SQL keywords + user input | **CRITICAL** | `f"SELECT * FROM users WHERE id = {user_id}"` | CWE-89 |
|
|
56
|
+
| String concat into SQL keyword string | **CRITICAL** | `"SELECT ... " + var + " ..."` | CWE-89 |
|
|
57
|
+
| %-format SQL string | **HIGH** | `"SELECT * FROM %s" % table_name` | CWE-89 |
|
|
58
|
+
| `.format()` into SQL string | **HIGH** | `"SELECT {} FROM users".format(col)` | CWE-89 |
|
|
59
|
+
| `cursor.execute(f"...")` | **CRITICAL** | f-string passed directly to cursor.execute | CWE-89 |
|
|
60
|
+
| `sequelize.query` with template literal | **HIGH** | `sequelize.query(\`SELECT * FROM ${table}\`)` | CWE-89 |
|
|
61
|
+
| Knex / sequelize raw() with interpolation | **HIGH** | `knex.raw('SELECT * FROM ' + table)` | CWE-89 |
|
|
62
|
+
| Django `.extra()` with raw SQL | **MEDIUM** | `Model.objects.extra(where=['col = ' + val])` | CWE-89 |
|
|
63
|
+
| `cursor.executemany` with string-built query | **CRITICAL** | Same risk as execute | CWE-89 |
|
|
64
|
+
| JDBC `Statement.execute` with concat | **HIGH** | Java pattern: not PreparedStatement | CWE-89 |
|
|
65
|
+
| Rails `where()` with string interpolation | **HIGH** | `User.where("name = '#{name}'")` | CWE-89 |
|
|
66
|
+
| Go `db.Query` with `fmt.Sprintf` | **HIGH** | `db.Query(fmt.Sprintf("...", arg))` | CWE-89 |
|
|
67
|
+
|
|
68
|
+
## Prerequisites
|
|
69
|
+
|
|
70
|
+
- Python 3.9+
|
|
71
|
+
- Target source tree on local filesystem
|
|
72
|
+
|
|
73
|
+
## Instructions
|
|
74
|
+
|
|
75
|
+
### Step 1 — Run the scanner
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py /path/to/repo
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Usage: scan_sqli.py PATH [OPTIONS]
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
--output FILE Write findings to FILE
|
|
88
|
+
--format FMT json | jsonl | markdown (default: markdown)
|
|
89
|
+
--min-severity SEV (default: info)
|
|
90
|
+
--include-tests Include test directories (default: excluded)
|
|
91
|
+
--languages LIST Comma-separated: python,javascript,typescript,java,
|
|
92
|
+
ruby,go,php,csharp (default: all)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Step 2 — Interpret findings
|
|
96
|
+
|
|
97
|
+
CRITICAL = direct user-input → query string construction. Fix the
|
|
98
|
+
specific query AND audit nearby code for the same pattern.
|
|
99
|
+
|
|
100
|
+
HIGH = pattern suggests interpolation but might be a fixed
|
|
101
|
+
identifier (table/column name). Verify by reading the code.
|
|
102
|
+
|
|
103
|
+
MEDIUM = framework-specific pattern that's safe ONLY with strict
|
|
104
|
+
input validation (Django `.extra()`, Rails string `where()`).
|
|
105
|
+
|
|
106
|
+
### Step 3 — Remediation
|
|
107
|
+
|
|
108
|
+
For each finding, the fix is the same shape per language: use the
|
|
109
|
+
language/library's parameterized-query API. See
|
|
110
|
+
`references/PLAYBOOK.md` for per-language snippets.
|
|
111
|
+
|
|
112
|
+
### Step 4 — Cross-skill chaining
|
|
113
|
+
|
|
114
|
+
Consider running `scanning-for-hardcoded-secrets` (#10) on the same
|
|
115
|
+
target — same audit, different class of finding.
|
|
116
|
+
|
|
117
|
+
## Examples
|
|
118
|
+
|
|
119
|
+
### Example 1 — Pre-merge code review
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py \
|
|
123
|
+
--min-severity high $(git diff --name-only main...HEAD | tr '\n' ' ')
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Scans only files changed in the current branch — fast feedback for
|
|
127
|
+
PR review.
|
|
128
|
+
|
|
129
|
+
### Example 2 — Legacy codebase audit
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py \
|
|
133
|
+
/path/to/legacy-app --format markdown > sqli-audit.md
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Expect dozens to hundreds of findings on a pre-ORM Java/PHP
|
|
137
|
+
codebase. Prioritize by reachability: the queries reached from
|
|
138
|
+
public endpoints first.
|
|
139
|
+
|
|
140
|
+
## Output
|
|
141
|
+
|
|
142
|
+
JSON / JSONL / Markdown. Exit codes: 0 clean, 1 high/critical, 2 error.
|
|
143
|
+
|
|
144
|
+
## Error Handling
|
|
145
|
+
|
|
146
|
+
- **False positives on fixed-identifier interpolation** (e.g.,
|
|
147
|
+
`f"SELECT * FROM {tablename}"` where `tablename` is hardcoded) →
|
|
148
|
+
verify manually. The scanner can't reason about variable
|
|
149
|
+
provenance without a full AST + control-flow pass.
|
|
150
|
+
- **String-built dynamic-table queries** are sometimes legitimate
|
|
151
|
+
(multi-tenant routing). Flag and review; the fix is usually
|
|
152
|
+
allow-list validation + identifier quoting.
|
|
153
|
+
|
|
154
|
+
## Resources
|
|
155
|
+
|
|
156
|
+
- `references/THEORY.md` — Per-language interpolation patterns,
|
|
157
|
+
ORM-specific safe vs unsafe APIs, why prepared statements work
|
|
158
|
+
- `references/PLAYBOOK.md` — Per-language parameterization snippets
|
|
159
|
+
(Python sqlite3 + psycopg + SQLAlchemy, Node mysql2 + pg + knex
|
|
160
|
+
- sequelize, Ruby ActiveRecord, Go database/sql, Java JDBC
|
|
161
|
+
PreparedStatement, PHP PDO)
|