attackmap 0.6.4__py3-none-any.whl
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.
- attackmap/__init__.py +2 -0
- attackmap/analyzer.py +449 -0
- attackmap/analyzer_contracts.py +194 -0
- attackmap/analyzers.py +1003 -0
- attackmap/anomalies.py +612 -0
- attackmap/asset_model.py +245 -0
- attackmap/attack_taxonomy.py +133 -0
- attackmap/auth_protocol.py +702 -0
- attackmap/authz.py +1695 -0
- attackmap/bench.py +467 -0
- attackmap/cli.py +1474 -0
- attackmap/config_scanner.py +389 -0
- attackmap/context_pack.py +82 -0
- attackmap/contracts.py +201 -0
- attackmap/control_model.py +280 -0
- attackmap/crossrepo.py +356 -0
- attackmap/crypto.py +240 -0
- attackmap/cve.py +717 -0
- attackmap/defensive_review.py +647 -0
- attackmap/detection_opportunities.py +1221 -0
- attackmap/detections/__init__.py +491 -0
- attackmap/detections/bola.yml +57 -0
- attackmap/detections/cloud.yml +609 -0
- attackmap/detections/injection.yml +167 -0
- attackmap/detections/secrets.yml +48 -0
- attackmap/detections/sigma.py +511 -0
- attackmap/detections/ssrf.yml +98 -0
- attackmap/detections/weakness.yml +62 -0
- attackmap/detections/webhook.yml +45 -0
- attackmap/detections/workflow.yml +121 -0
- attackmap/diagrams.py +237 -0
- attackmap/diff.py +301 -0
- attackmap/exploitability.py +493 -0
- attackmap/fingerprint.py +91 -0
- attackmap/fleet.py +370 -0
- attackmap/gitref.py +129 -0
- attackmap/graph.py +26 -0
- attackmap/handler_auth.py +639 -0
- attackmap/handler_span.py +62 -0
- attackmap/hunt_harness.py +505 -0
- attackmap/insights.py +701 -0
- attackmap/llm_review.py +846 -0
- attackmap/lockfiles.py +521 -0
- attackmap/mass_assignment.py +543 -0
- attackmap/md.py +175 -0
- attackmap/merge.py +333 -0
- attackmap/models.py +1284 -0
- attackmap/plugins_lock.py +114 -0
- attackmap/popular_packages.py +193 -0
- attackmap/progress.py +258 -0
- attackmap/recon_models.py +31 -0
- attackmap/recon_to_analysis.py +149 -0
- attackmap/redact.py +220 -0
- attackmap/report.py +424 -0
- attackmap/review_eval.py +335 -0
- attackmap/review_json.py +308 -0
- attackmap/review_prompts.py +796 -0
- attackmap/route_auth_fusion.py +474 -0
- attackmap/safe_fs.py +285 -0
- attackmap/sarif.py +373 -0
- attackmap/sbom.py +418 -0
- attackmap/scanner.py +1379 -0
- attackmap/sdk/__init__.py +278 -0
- attackmap/sdk/contracts.py +17 -0
- attackmap/sdk/fs.py +156 -0
- attackmap/sdk/models.py +59 -0
- attackmap/secrets_history.py +161 -0
- attackmap/security_overlay.py +71 -0
- attackmap/srcpaths.py +301 -0
- attackmap/suggest.py +394 -0
- attackmap/supply_chain.py +688 -0
- attackmap/suppress.py +740 -0
- attackmap/taint.py +1540 -0
- attackmap/taint_flow.py +1419 -0
- attackmap/taint_sinks.py +431 -0
- attackmap/taint_sinks.yaml +308 -0
- attackmap/taint_sources.py +396 -0
- attackmap/taxonomy.py +465 -0
- attackmap/threat_model.py +3441 -0
- attackmap/topology.py +316 -0
- attackmap/triage.py +151 -0
- attackmap/weaknesses.py +365 -0
- attackmap/webhardening.py +479 -0
- attackmap/webhook_verification.py +1067 -0
- attackmap/workflow_scanner.py +964 -0
- attackmap-0.6.4.dist-info/METADATA +226 -0
- attackmap-0.6.4.dist-info/RECORD +91 -0
- attackmap-0.6.4.dist-info/WHEEL +5 -0
- attackmap-0.6.4.dist-info/entry_points.txt +2 -0
- attackmap-0.6.4.dist-info/licenses/LICENSE +21 -0
- attackmap-0.6.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Per-language taint source, sanitizer and guard catalogs (#239).
|
|
2
|
+
|
|
3
|
+
The intra-procedural flow pass (:mod:`attackmap.taint_flow`) marks a local
|
|
4
|
+
variable tainted when the right-hand side of its assignment matches one of the
|
|
5
|
+
**sources** below, then follows simple assignments to the sink. A
|
|
6
|
+
**sanitizer** neutralizes the value only when it wraps the tainted value on
|
|
7
|
+
the way to the sink (``u = secure_filename(u)``), and a **guard** only when it
|
|
8
|
+
is an early-exit (or enclosing) check on a variable derived from the same
|
|
9
|
+
source (``if ip.is_private: abort(400)``). Nothing here is file-granular.
|
|
10
|
+
|
|
11
|
+
Every entry is a ``(label, compiled-regex[, weight])`` tuple so that
|
|
12
|
+
``tests/test_regex_budget.py`` auto-discovers and budget-checks each pattern.
|
|
13
|
+
|
|
14
|
+
Source weights:
|
|
15
|
+
|
|
16
|
+
- ``"high"`` — attacker-controlled request input (query, body, path params,
|
|
17
|
+
headers, cookies, uploaded filenames, queue/event payloads). Satisfies the
|
|
18
|
+
request-taint gate on SSRF / SSTI / path / redirect / NoSQL sinks.
|
|
19
|
+
- ``"low"`` — process environment and argv. Recorded on a chain as its source
|
|
20
|
+
when nothing stronger reaches the sink, but never satisfies the gate: an
|
|
21
|
+
operator-set env var is not attacker input for a web route.
|
|
22
|
+
|
|
23
|
+
To extend: add a tuple under the language. Keep patterns specific (a member
|
|
24
|
+
access on a request-shaped receiver, not a bare identifier) and linear — no
|
|
25
|
+
nested quantifiers.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
# Language keys used throughout the flow pass.
|
|
33
|
+
PY, JS, GO, PHP, JAVA = "python", "js", "go", "php", "java"
|
|
34
|
+
# Indexed for sinks (#240); C# also gets flow analysis, Ruby only the
|
|
35
|
+
# same-call request-token gate (no brace-delimited functions to scope).
|
|
36
|
+
CSHARP, RUBY = "csharp", "ruby"
|
|
37
|
+
|
|
38
|
+
_LANG_BY_SUFFIX = {
|
|
39
|
+
".py": PY,
|
|
40
|
+
".js": JS, ".jsx": JS, ".mjs": JS, ".cjs": JS, ".ts": JS, ".tsx": JS, ".mts": JS, ".cts": JS,
|
|
41
|
+
".go": GO,
|
|
42
|
+
".php": PHP,
|
|
43
|
+
".java": JAVA,
|
|
44
|
+
".cs": CSHARP,
|
|
45
|
+
".rb": RUBY,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def lang_for_suffix(suffix: str) -> str | None:
|
|
50
|
+
"""The flow-pass language for a file suffix, or None if unsupported."""
|
|
51
|
+
return _LANG_BY_SUFFIX.get(suffix.lower())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- Sources -----------------------------------------------------------------
|
|
55
|
+
# (kind, pattern, weight). Python patterns are applied with ``re.match`` to the
|
|
56
|
+
# source text of an expression node (so they are anchored at the expression
|
|
57
|
+
# start); the other languages are searched in statement text with string
|
|
58
|
+
# literals blanked out.
|
|
59
|
+
|
|
60
|
+
_AW = r"(?:await\s+)?"
|
|
61
|
+
|
|
62
|
+
SOURCES: dict[str, tuple[tuple[str, re.Pattern[str], str], ...]] = {
|
|
63
|
+
PY: (
|
|
64
|
+
("query", re.compile(_AW + r"request\.(?:args|GET|query_params|query_string|values)\b"), "high"),
|
|
65
|
+
("body", re.compile(_AW + r"request\.(?:form|json|data|POST|body|get_json|get_data|stream|media)\b"), "high"),
|
|
66
|
+
("upload_filename", re.compile(_AW + r"request\.(?:files|FILES)\b"), "high"),
|
|
67
|
+
("header", re.compile(_AW + r"request\.(?:headers|META|environ)\b"), "high"),
|
|
68
|
+
("cookie", re.compile(_AW + r"request\.(?:cookies|COOKIES)\b"), "high"),
|
|
69
|
+
("path_param", re.compile(_AW + r"request\.(?:view_args|path_params|match_info|path|full_path|url)\b"), "high"),
|
|
70
|
+
# aiohttp / falcon / starlette style `req`
|
|
71
|
+
("query", re.compile(_AW + r"req\.(?:params|query|query_params|get_param\w*|rel_url)\b"), "high"),
|
|
72
|
+
("body", re.compile(_AW + r"req\.(?:media|json|text|post|content|stream|form)\b"), "high"),
|
|
73
|
+
("header", re.compile(_AW + r"req\.(?:headers|get_header)\b"), "high"),
|
|
74
|
+
("cookie", re.compile(_AW + r"req\.cookies\b"), "high"),
|
|
75
|
+
("path_param", re.compile(_AW + r"req\.(?:match_info|path_params)\b"), "high"),
|
|
76
|
+
# AWS Lambda / API Gateway events and queue messages
|
|
77
|
+
(
|
|
78
|
+
"body",
|
|
79
|
+
re.compile(
|
|
80
|
+
r"event\s*(?:\[\s*|\.get\s*\(\s*)[\"'](?:body|queryStringParameters|pathParameters|headers|multiValueQueryStringParameters)[\"']"
|
|
81
|
+
),
|
|
82
|
+
"high",
|
|
83
|
+
),
|
|
84
|
+
("message", re.compile(r"(?:message|msg|record)\s*\[\s*[\"'](?:body|data|payload|value)[\"']"), "high"),
|
|
85
|
+
("message", re.compile(r"(?:message|msg)\.(?:value|body|data|payload)\b"), "high"),
|
|
86
|
+
("env", re.compile(r"os\.(?:environ|getenv)\b"), "low"),
|
|
87
|
+
("argv", re.compile(r"sys\.argv\b"), "low"),
|
|
88
|
+
),
|
|
89
|
+
JS: (
|
|
90
|
+
("query", re.compile(r"\breq(?:uest)?\s*\.\s*query\b"), "high"),
|
|
91
|
+
("query", re.compile(r"\bctx\s*\.\s*(?:request\s*\.\s*)?query(?:string)?\b"), "high"),
|
|
92
|
+
("query", re.compile(r"\bsearchParams\s*\.\s*get(?:All)?\s*\("), "high"),
|
|
93
|
+
("body", re.compile(r"\breq(?:uest)?\s*\.\s*body\b"), "high"),
|
|
94
|
+
("body", re.compile(r"\bctx\s*\.\s*request\s*\.\s*body\b"), "high"),
|
|
95
|
+
("body", re.compile(r"\bawait\s+req(?:uest)?\s*\.\s*(?:json|formData|text)\s*\("), "high"),
|
|
96
|
+
("path_param", re.compile(r"\breq(?:uest)?\s*\.\s*params\b"), "high"),
|
|
97
|
+
("path_param", re.compile(r"\bctx\s*\.\s*params\b"), "high"),
|
|
98
|
+
("path_param", re.compile(r"\breq\s*\.\s*(?:path|originalUrl|url)\b"), "high"),
|
|
99
|
+
("header", re.compile(r"\breq(?:uest)?\s*\.\s*headers\b"), "high"),
|
|
100
|
+
("header", re.compile(r"\breq\s*\.\s*(?:get|header)\s*\("), "high"),
|
|
101
|
+
("header", re.compile(r"\bctx\s*\.\s*(?:headers|get\s*\()"), "high"),
|
|
102
|
+
("cookie", re.compile(r"\breq(?:uest)?\s*\.\s*(?:cookies|signedCookies)\b"), "high"),
|
|
103
|
+
("cookie", re.compile(r"\bctx\s*\.\s*cookies\s*\.\s*get\s*\("), "high"),
|
|
104
|
+
("upload_filename", re.compile(r"\breq\s*\.\s*files?\b"), "high"),
|
|
105
|
+
("message", re.compile(r"\b(?:message|msg)\s*\.\s*(?:value|body|content)\b"), "high"),
|
|
106
|
+
("env", re.compile(r"\bprocess\s*\.\s*env\b"), "low"),
|
|
107
|
+
("argv", re.compile(r"\bprocess\s*\.\s*argv\b"), "low"),
|
|
108
|
+
),
|
|
109
|
+
GO: (
|
|
110
|
+
("query", re.compile(r"\b\w+\.URL\.(?:Query\s*\(\s*\)|RawQuery)"), "high"),
|
|
111
|
+
("query", re.compile(r"\b(?:c|ctx)\.(?:Query|DefaultQuery|QueryParam|QueryArray|GetQuery)\s*\("), "high"),
|
|
112
|
+
("body", re.compile(r"\b\w+\.(?:FormValue|PostFormValue)\s*\("), "high"),
|
|
113
|
+
("body", re.compile(r"\b(?:r|req|request)\.(?:Body|Form|PostForm|MultipartForm)\b"), "high"),
|
|
114
|
+
("body", re.compile(r"\b(?:c|ctx)\.(?:PostForm|DefaultPostForm|GetRawData|Body)\s*\("), "high"),
|
|
115
|
+
("path_param", re.compile(r"\bmux\.Vars\s*\("), "high"),
|
|
116
|
+
("path_param", re.compile(r"\bchi\.URLParam\s*\("), "high"),
|
|
117
|
+
("path_param", re.compile(r"\b(?:c|ctx)\.(?:Param|Params)\s*\("), "high"),
|
|
118
|
+
("path_param", re.compile(r"\b(?:r|req|request)\.(?:URL\.Path|RequestURI|PathValue\s*\()"), "high"),
|
|
119
|
+
("header", re.compile(r"\b(?:r|req|request)\.Header\.(?:Get|Values)\s*\("), "high"),
|
|
120
|
+
("header", re.compile(r"\b(?:c|ctx)\.(?:GetHeader|Get)\s*\("), "high"),
|
|
121
|
+
("cookie", re.compile(r"\b(?:r|req|request)\.Cookies?\s*\("), "high"),
|
|
122
|
+
("cookie", re.compile(r"\b(?:c|ctx)\.Cookie\s*\("), "high"),
|
|
123
|
+
("upload_filename", re.compile(r"\b\w+\.FormFile\s*\("), "high"),
|
|
124
|
+
("env", re.compile(r"\bos\.(?:Getenv|LookupEnv|Environ)\s*\("), "low"),
|
|
125
|
+
("argv", re.compile(r"\bos\.Args\b|\bflag\.(?:Arg|Args)\s*\("), "low"),
|
|
126
|
+
),
|
|
127
|
+
PHP: (
|
|
128
|
+
("query", re.compile(r"\$_GET\b"), "high"),
|
|
129
|
+
("body", re.compile(r"\$_(?:POST|REQUEST)\b"), "high"),
|
|
130
|
+
("body", re.compile(r"php://input"), "high"),
|
|
131
|
+
("cookie", re.compile(r"\$_COOKIE\b"), "high"),
|
|
132
|
+
("upload_filename", re.compile(r"\$_FILES\b"), "high"),
|
|
133
|
+
("header", re.compile(r"\$_SERVER\s*\[\s*['\"](?:HTTP_\w+|REQUEST_URI|QUERY_STRING|PATH_INFO|PHP_SELF)['\"]"), "high"),
|
|
134
|
+
# Laravel / Symfony request objects
|
|
135
|
+
(
|
|
136
|
+
"body",
|
|
137
|
+
re.compile(r"\$request\s*->\s*(?:input|get|post|all|only|json|getContent|validated)\s*\("),
|
|
138
|
+
"high",
|
|
139
|
+
),
|
|
140
|
+
("query", re.compile(r"\$request\s*->\s*(?:query)\b"), "high"),
|
|
141
|
+
("header", re.compile(r"\$request\s*->\s*(?:header|headers)\b"), "high"),
|
|
142
|
+
("cookie", re.compile(r"\$request\s*->\s*(?:cookie|cookies)\b"), "high"),
|
|
143
|
+
("path_param", re.compile(r"\$request\s*->\s*(?:route|attributes|getPathInfo|getRequestUri)\b"), "high"),
|
|
144
|
+
("upload_filename", re.compile(r"\$request\s*->\s*(?:file|files)\b"), "high"),
|
|
145
|
+
("body", re.compile(r"\$request\s*->\s*request\s*->\s*get\s*\("), "high"),
|
|
146
|
+
("env", re.compile(r"\bgetenv\s*\(|\$_ENV\b"), "low"),
|
|
147
|
+
("argv", re.compile(r"\$argv\b"), "low"),
|
|
148
|
+
),
|
|
149
|
+
JAVA: (
|
|
150
|
+
(
|
|
151
|
+
"query",
|
|
152
|
+
re.compile(r"\b\w+\s*\.\s*(?:getParameter|getParameterValues|getParameterMap|getQueryString)\s*\("),
|
|
153
|
+
"high",
|
|
154
|
+
),
|
|
155
|
+
(
|
|
156
|
+
"body",
|
|
157
|
+
re.compile(
|
|
158
|
+
r"\b(?:request|req|httpRequest|servletRequest|httpServletRequest)\s*\.\s*"
|
|
159
|
+
r"(?:getInputStream|getReader|getPart|getParts)\s*\("
|
|
160
|
+
),
|
|
161
|
+
"high",
|
|
162
|
+
),
|
|
163
|
+
("path_param", re.compile(r"\b\w+\s*\.\s*(?:getRequestURI|getPathInfo|getServletPath)\s*\("), "high"),
|
|
164
|
+
(
|
|
165
|
+
"header",
|
|
166
|
+
re.compile(r"\b(?:request|req|httpRequest|servletRequest|httpServletRequest)\s*\.\s*getHeaders?\s*\("),
|
|
167
|
+
"high",
|
|
168
|
+
),
|
|
169
|
+
("cookie", re.compile(r"\b\w+\s*\.\s*getCookies\s*\(\s*\)"), "high"),
|
|
170
|
+
("env", re.compile(r"\bSystem\s*\.\s*(?:getenv|getProperty)\s*\("), "low"),
|
|
171
|
+
),
|
|
172
|
+
CSHARP: (
|
|
173
|
+
("query", re.compile(r"\bRequest\s*\.\s*(?:Query|QueryString)\b"), "high"),
|
|
174
|
+
("body", re.compile(r"\bRequest\s*\.\s*(?:Form|Body|ReadFromJsonAsync)\b"), "high"),
|
|
175
|
+
("header", re.compile(r"\bRequest\s*\.\s*Headers\b"), "high"),
|
|
176
|
+
("cookie", re.compile(r"\bRequest\s*\.\s*Cookies\b"), "high"),
|
|
177
|
+
("path_param", re.compile(r"\bRequest\s*\.\s*(?:RouteValues|Path)\b"), "high"),
|
|
178
|
+
("env", re.compile(r"\bEnvironment\s*\.\s*GetEnvironmentVariable\s*\("), "low"),
|
|
179
|
+
),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Handler parameters bound from the request by a framework annotation /
|
|
183
|
+
# decorator (Spring, JAX-RS, NestJS). The parameter name follows the
|
|
184
|
+
# annotation; the flow pass extracts it procedurally.
|
|
185
|
+
PARAM_ANNOTATIONS: dict[str, tuple[tuple[str, re.Pattern[str]], ...]] = {
|
|
186
|
+
JAVA: (
|
|
187
|
+
("query", re.compile(r"@(?:RequestParam|QueryParam|MatrixParam)\b")),
|
|
188
|
+
("path_param", re.compile(r"@(?:PathVariable|PathParam)\b")),
|
|
189
|
+
("body", re.compile(r"@(?:RequestBody|ModelAttribute|RequestPart|FormParam|BeanParam)\b")),
|
|
190
|
+
("header", re.compile(r"@(?:RequestHeader|HeaderParam)\b")),
|
|
191
|
+
("cookie", re.compile(r"@(?:CookieValue|CookieParam)\b")),
|
|
192
|
+
),
|
|
193
|
+
CSHARP: (
|
|
194
|
+
("query", re.compile(r"\[\s*FromQuery\b")),
|
|
195
|
+
("path_param", re.compile(r"\[\s*FromRoute\b")),
|
|
196
|
+
("body", re.compile(r"\[\s*From(?:Body|Form)\b")),
|
|
197
|
+
("header", re.compile(r"\[\s*FromHeader\b")),
|
|
198
|
+
),
|
|
199
|
+
JS: (
|
|
200
|
+
("query", re.compile(r"@Query\s*\(")),
|
|
201
|
+
("path_param", re.compile(r"@Param\s*\(")),
|
|
202
|
+
("body", re.compile(r"@Body\s*\(")),
|
|
203
|
+
("header", re.compile(r"@Headers\s*\(")),
|
|
204
|
+
("upload_filename", re.compile(r"@UploadedFiles?\s*\(")),
|
|
205
|
+
),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Python route decorators (the decorator call's final attribute name, matched
|
|
209
|
+
# on the AST) — a decorated function is a request handler and its
|
|
210
|
+
# (non-framework) parameters are request-bound: path params, FastAPI query /
|
|
211
|
+
# body params.
|
|
212
|
+
PY_ROUTE_DECORATOR_NAMES = frozenset(
|
|
213
|
+
{"route", "get", "post", "put", "patch", "delete", "head", "options", "api_route", "websocket"}
|
|
214
|
+
)
|
|
215
|
+
# Parameters that are framework plumbing, not attacker data.
|
|
216
|
+
PY_PLUMBING_PARAMS = frozenset(
|
|
217
|
+
{"self", "cls", "request", "req", "db", "session", "response", "background_tasks", "current_user", "user"}
|
|
218
|
+
)
|
|
219
|
+
# Annotations that coerce (int / UUID / date) or mark plumbing — such a
|
|
220
|
+
# parameter is not an injectable string.
|
|
221
|
+
PY_SAFE_ANNOTATION = re.compile(
|
|
222
|
+
r"\b(?:int|float|bool|UUID|uuid\.UUID|datetime|date|time|Decimal|PositiveInt|NonNegativeInt|conint|"
|
|
223
|
+
r"Request|Response|Session|AsyncSession|BackgroundTasks|WebSocket|HTTPConnection|Depends|Security)\b"
|
|
224
|
+
)
|
|
225
|
+
PY_SAFE_DEFAULT = re.compile(r"\b(?:Depends|Security)\s*\(")
|
|
226
|
+
PY_SCALAR_ANNOTATION = re.compile(r"\b(?:str|list|List|Optional|Query|Path)\b")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# --- Sanitizers (flow-bound wrappers) ----------------------------------------
|
|
230
|
+
# A sanitizer counts only when the tainted value passes *through* the call on
|
|
231
|
+
# the way to the sink: ``safe = shlex.quote(host)`` then ``run(... safe ...)``,
|
|
232
|
+
# or the wrapper inline in the sink argument. Each pattern matches the callee
|
|
233
|
+
# name up to (and including) its opening paren.
|
|
234
|
+
|
|
235
|
+
SANITIZERS: dict[str, tuple[tuple[str, re.Pattern[str]], ...]] = {
|
|
236
|
+
"subprocess_shell": (
|
|
237
|
+
("shlex.quote", re.compile(r"\bshlex\.quote\s*\(")),
|
|
238
|
+
("pipes.quote", re.compile(r"\bpipes\.quote\s*\(")),
|
|
239
|
+
("escapeshellarg/escapeshellcmd", re.compile(r"\b(?:escapeshellarg|escapeshellcmd)\s*\(")),
|
|
240
|
+
("shell-escape", re.compile(r"\b(?:shellEscape|shellescape|shellQuote\.quote)\s*\(")),
|
|
241
|
+
),
|
|
242
|
+
"sql_execute": (
|
|
243
|
+
("mysqli_real_escape_string", re.compile(r"\bmysqli_real_escape_string\s*\(")),
|
|
244
|
+
("pg_escape_*", re.compile(r"\bpg_escape_(?:string|literal|identifier)\s*\(")),
|
|
245
|
+
("PDO::quote", re.compile(r"->\s*quote\s*\(")),
|
|
246
|
+
("sql escape()", re.compile(r"\b(?:mysql|connection|conn|pool|SqlString)\s*\.\s*escape(?:Id)?\s*\(")),
|
|
247
|
+
),
|
|
248
|
+
"dynamic_open": (
|
|
249
|
+
("werkzeug.secure_filename", re.compile(r"\bsecure_filename\s*\(")),
|
|
250
|
+
("os.path.basename", re.compile(r"\bos\.path\.basename\s*\(")),
|
|
251
|
+
("path.basename", re.compile(r"\bpath\s*\.\s*basename\s*\(")),
|
|
252
|
+
("filepath.Base", re.compile(r"\bfilepath\.Base\s*\(")),
|
|
253
|
+
("basename()", re.compile(r"(?<![\w.>$])basename\s*\(")),
|
|
254
|
+
("FilenameUtils.getName", re.compile(r"\bFilenameUtils\s*\.\s*getName\s*\(")),
|
|
255
|
+
),
|
|
256
|
+
# Only a real escaper of the template *source*. `re.escape` / `html.escape`
|
|
257
|
+
# are deliberately absent (#239): neither neutralizes `{{ ... }}`, and their
|
|
258
|
+
# presence elsewhere in a file used to silence real SSTI chains.
|
|
259
|
+
"ssti": (
|
|
260
|
+
("markupsafe.escape", re.compile(r"\b(?:markupsafe|Markup)\.escape\s*\(|(?<![\w.])escape\s*\(")),
|
|
261
|
+
("bleach.clean", re.compile(r"\bbleach\.clean\s*\(")),
|
|
262
|
+
),
|
|
263
|
+
"nosql_injection": (
|
|
264
|
+
("mongo-sanitize", re.compile(r"\b(?:mongoSanitize|mongo_sanitize|sanitize)\s*\(")),
|
|
265
|
+
),
|
|
266
|
+
"open_redirect": (
|
|
267
|
+
("url_for", re.compile(r"\burl_for\s*\(")),
|
|
268
|
+
),
|
|
269
|
+
# #240 kinds
|
|
270
|
+
"regex_injection": (
|
|
271
|
+
("regex escape", re.compile(
|
|
272
|
+
r"\bre\.escape\s*\(|\b(?:_|lodash)\.escapeRegExp\s*\(|\bescapeStringRegexp\s*\(|\bescapeRegExp\s*\("
|
|
273
|
+
r"|\bPattern\s*\.\s*quote\s*\(|\bregexp\.QuoteMeta\s*\(|\bpreg_quote\s*\("
|
|
274
|
+
)),
|
|
275
|
+
),
|
|
276
|
+
"ldap_injection": (
|
|
277
|
+
("LDAP filter escape", re.compile(
|
|
278
|
+
r"\bescape_filter_chars\s*\(|\bldap_escape\s*\(|\bLdapEncoder\s*\.\s*filterEncode\s*\("
|
|
279
|
+
r"|\bEncode\s*\.\s*forLdap\s*\(|\bescape_filter\s*\("
|
|
280
|
+
)),
|
|
281
|
+
),
|
|
282
|
+
"xpath_injection": (
|
|
283
|
+
("XPath escape", re.compile(r"\bEncode\s*\.\s*forXPath\s*\(|\bxpath_escape\s*\(")),
|
|
284
|
+
),
|
|
285
|
+
"header_injection": (
|
|
286
|
+
("URL-encode", re.compile(r"\b(?:encodeURIComponent|urllib\.parse\.quote|quote|rawurlencode|urlencode|url\.QueryEscape|URLEncoder\s*\.\s*encode)\s*\(")),
|
|
287
|
+
),
|
|
288
|
+
}
|
|
289
|
+
SANITIZERS["path_traversal"] = SANITIZERS["dynamic_open"]
|
|
290
|
+
|
|
291
|
+
# Type coercions neutralize every string-injection kind: an int can't carry
|
|
292
|
+
# a payload. Applied for all sink kinds.
|
|
293
|
+
CAST_SANITIZERS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
294
|
+
("int() cast", re.compile(r"(?<![\w.])(?:int|float|bool)\s*\(")),
|
|
295
|
+
("UUID cast", re.compile(r"\b(?:uuid\.)?UUID\s*\(|\bUUID\s*\.\s*fromString\s*\(")),
|
|
296
|
+
(
|
|
297
|
+
"Java numeric parse",
|
|
298
|
+
re.compile(r"\b(?:Integer|Long|Short|Double|Float)\s*\.\s*(?:parseInt|parseLong|parseShort|parseDouble|parseFloat|valueOf)\s*\("),
|
|
299
|
+
),
|
|
300
|
+
("parseInt/Number cast", re.compile(r"\b(?:parseInt|parseFloat|Number)\s*\(")),
|
|
301
|
+
("strconv cast", re.compile(r"\bstrconv\.(?:Atoi|ParseInt|ParseUint|ParseFloat|ParseBool)\s*\(")),
|
|
302
|
+
("intval cast", re.compile(r"\b(?:intval|floatval|boolval)\s*\(|\(\s*(?:int|float)\s*\)\s*\$")),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# --- Guards (flow-bound checks dominating the sink) --------------------------
|
|
307
|
+
# A guard neutralizes when its condition references a variable derived from
|
|
308
|
+
# the same source as the sink argument AND it dominates the sink: either an
|
|
309
|
+
# early exit (`if <cond>: abort/return/raise`) before the sink, or an `if`
|
|
310
|
+
# whose body contains the sink.
|
|
311
|
+
|
|
312
|
+
GUARDS: dict[str, tuple[tuple[str, re.Pattern[str]], ...]] = {
|
|
313
|
+
# SSRF needs an actual address-class or allow-list decision — the mere
|
|
314
|
+
# presence of `ipaddress.ip_address(` (often just logging) is not enough.
|
|
315
|
+
"ssrf": (
|
|
316
|
+
(
|
|
317
|
+
"private/loopback/link-local address check",
|
|
318
|
+
re.compile(
|
|
319
|
+
r"\.is_(?:private|loopback|link_local|reserved|multicast|unspecified|global)\b"
|
|
320
|
+
r"|\.Is(?:Private|Loopback|LinkLocalUnicast|LinkLocalMulticast|Unspecified|GlobalUnicast)\s*\("
|
|
321
|
+
r"|\.is(?:SiteLocal|Loopback|LinkLocal|AnyLocal)Address\s*\("
|
|
322
|
+
),
|
|
323
|
+
),
|
|
324
|
+
("outbound URL allow-list", re.compile(r"\b(?:is_allowed_url|is_safe_url|isAllowedUrl|isSafeUrl)\s*\(")),
|
|
325
|
+
),
|
|
326
|
+
"open_redirect": (
|
|
327
|
+
("is_safe_url", re.compile(r"\bis_safe_url\s*\(")),
|
|
328
|
+
("url_has_allowed_host_and_scheme", re.compile(r"\burl_has_allowed_host_and_scheme\s*\(")),
|
|
329
|
+
("relative-path check", re.compile(r"\.startswith\s*\(\s*[\"']/[\"']|\.startsWith\s*\(\s*[\"']/[\"']")),
|
|
330
|
+
),
|
|
331
|
+
"dynamic_open": (
|
|
332
|
+
(
|
|
333
|
+
"path containment check",
|
|
334
|
+
re.compile(r"\b(?:startswith|startsWith|HasPrefix|str_starts_with|is_relative_to|commonpath)\s*\("),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
"subprocess_shell": (),
|
|
338
|
+
"sql_execute": (),
|
|
339
|
+
}
|
|
340
|
+
GUARDS["path_traversal"] = GUARDS["dynamic_open"]
|
|
341
|
+
GUARDS["zip_slip"] = GUARDS["dynamic_open"]
|
|
342
|
+
|
|
343
|
+
# Guards valid for every sink kind: an explicit allow-list membership test or a
|
|
344
|
+
# full-match regex validation of the value.
|
|
345
|
+
GENERIC_GUARDS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
346
|
+
(
|
|
347
|
+
"allow-list check",
|
|
348
|
+
re.compile(
|
|
349
|
+
r"\b(?:ALLOWED_\w+|ALLOW_?LIST\w*|allowed_\w+|allowlist\w*|allowList\w*|whitelist\w*|"
|
|
350
|
+
r"allowedHosts|allowedDomains|allowedCommands|allowedFiles)\b"
|
|
351
|
+
),
|
|
352
|
+
),
|
|
353
|
+
(
|
|
354
|
+
"regex validation",
|
|
355
|
+
re.compile(r"\bre\.fullmatch\s*\(|\bpreg_match\s*\(|\.MatchString\s*\(|\.matches\s*\(\s*\"\^"),
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Tokens that make a guard's branch an early exit.
|
|
360
|
+
EARLY_EXIT = re.compile(
|
|
361
|
+
r"\b(?:return|throw|raise|abort|exit|die|panic|continue|break)\b"
|
|
362
|
+
r"|\bhttp\.Error\s*\(|\.sendStatus\s*\(|\.AbortWith\w*\s*\(|\bHttpResponseBadRequest\b"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def sanitizer_patterns(kind: str) -> tuple[tuple[str, re.Pattern[str]], ...]:
|
|
367
|
+
"""Flow-bound wrappers that neutralize ``kind`` (kind-specific + casts)."""
|
|
368
|
+
return SANITIZERS.get(kind, ()) + CAST_SANITIZERS
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def guard_patterns(kind: str) -> tuple[tuple[str, re.Pattern[str]], ...]:
|
|
372
|
+
"""Dominating checks that neutralize ``kind`` (kind-specific + generic)."""
|
|
373
|
+
return GUARDS.get(kind, ()) + GENERIC_GUARDS
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
ALL_SANITIZER_KINDS: tuple[str, ...] = (
|
|
377
|
+
"sql_execute",
|
|
378
|
+
"subprocess_shell",
|
|
379
|
+
"eval",
|
|
380
|
+
"exec",
|
|
381
|
+
"dynamic_open",
|
|
382
|
+
"unsafe_deserialization",
|
|
383
|
+
"ssti",
|
|
384
|
+
"ssrf",
|
|
385
|
+
"nosql_injection",
|
|
386
|
+
"open_redirect",
|
|
387
|
+
"path_traversal",
|
|
388
|
+
"zip_slip",
|
|
389
|
+
"code_injection",
|
|
390
|
+
"expression_injection",
|
|
391
|
+
"jndi_injection",
|
|
392
|
+
"ldap_injection",
|
|
393
|
+
"xpath_injection",
|
|
394
|
+
"header_injection",
|
|
395
|
+
"regex_injection",
|
|
396
|
+
)
|