adl-toolkit 0.0.0-stage → 0.1.1
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/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/skills/drift.md +25 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- package/scripts/install_toolkit.py +3584 -0
|
@@ -0,0 +1,1424 @@
|
|
|
1
|
+
"""Java CFG construction (P4.6, PROBLEMS.md).
|
|
2
|
+
|
|
3
|
+
`JavaCFGBuilder` is `graph.cfg.CFGBuilderBase`'s Java counterpart to
|
|
4
|
+
`lang/python/cfg.py::PythonCFGBuilder` — walks a `method_declaration`/
|
|
5
|
+
`constructor_declaration`/`compact_constructor_declaration` CST node into
|
|
6
|
+
basic blocks, reusing the shared `CFGBlock`/`CFGDef` bookkeeping and feeding
|
|
7
|
+
the shared `ReachingDefinitions` fixpoint (`graph/cfg.py`) exactly like
|
|
8
|
+
Python does.
|
|
9
|
+
|
|
10
|
+
Minimum statement kinds (codegraph-java-deep-dive.md §10.2), all handled
|
|
11
|
+
below: `local_variable_declaration`, `expression_statement`, `if_statement`,
|
|
12
|
+
`for_statement`, `enhanced_for_statement`, `while_statement`, `do_statement`,
|
|
13
|
+
`switch_expression`/`switch_statement`, `try_statement`,
|
|
14
|
+
`try_with_resources_statement`, `catch_clause`, `finally_clause`,
|
|
15
|
+
`synchronized_statement`, `return_statement`, `throw_statement`,
|
|
16
|
+
`break_statement`, `continue_statement`, `lambda_expression`.
|
|
17
|
+
|
|
18
|
+
Design notes:
|
|
19
|
+
- `return`/`throw` link to the function's synthetic exit block; `break`
|
|
20
|
+
targets the nearest enclosing loop or `switch`'s merge block (the
|
|
21
|
+
statement after it); `continue` targets the nearest enclosing loop's
|
|
22
|
+
condition re-check (a dedicated `update`-entry block for `for`, the
|
|
23
|
+
loop header for `while`/enhanced-`for`/`do`) — each threaded down as
|
|
24
|
+
`exit_b`/`break_target`/`continue_target` through every `_process_*`
|
|
25
|
+
helper, re-pointed at the right block by whichever compound statement
|
|
26
|
+
establishes it. None of these targets are always their "obvious"
|
|
27
|
+
block, though: a `try` with a `finally` (`_process_try`) substitutes
|
|
28
|
+
its own finally block(s) as the target for everything nested inside
|
|
29
|
+
its body/catch handlers, so an abrupt completion of any of the four
|
|
30
|
+
kinds still runs `finally` first — chaining correctly through
|
|
31
|
+
arbitrarily nested `try`/`finally` — before whatever it ultimately
|
|
32
|
+
reaches links onward to the real target.
|
|
33
|
+
- Labeled `break <label>;`/`continue <label>;` targeting an *enclosing
|
|
34
|
+
loop or switch* resolve to that specific statement's own targets, not
|
|
35
|
+
the nearest one — `labeled_statement` records `label -> (break_target,
|
|
36
|
+
continue_target)` into a `labels` map (also threaded through every
|
|
37
|
+
`_process_*` helper, and relayed through an intervening `try`/`finally`
|
|
38
|
+
exactly like `exit_b`/`break_target`/`continue_target` are) as soon as
|
|
39
|
+
it dispatches into the loop/switch it wraps. A label on any *other*
|
|
40
|
+
statement kind (`outer: if (...) { break outer; }` — legal Java, but
|
|
41
|
+
rare) is not supported: `label` is only threaded into the five
|
|
42
|
+
loop/switch handlers, so such a label is silently dropped and any
|
|
43
|
+
`break`/`continue` naming it falls back to the nearest enclosing
|
|
44
|
+
target, same as today.
|
|
45
|
+
- `switch`'s case/rule bodies are each treated as independently reachable
|
|
46
|
+
from the switch entry and merge into one successor block — the same
|
|
47
|
+
conservative "all handlers reachable, no attempt at precise fallthrough
|
|
48
|
+
edges" choice `PythonCFGBuilder._process_try` already makes for
|
|
49
|
+
`except`/`finally` clauses.
|
|
50
|
+
- A `lambda_expression` is an opaque leaf for def/use purposes (no
|
|
51
|
+
descent into its body), matching how `PythonCFGBuilder`/`_collect_uses`
|
|
52
|
+
already treats a nested `function_definition`/`lambda`/`class_definition`
|
|
53
|
+
as a value, not a control-flow site to analyze inline.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from typing import Dict, List, Literal, Optional, Tuple, Union
|
|
59
|
+
|
|
60
|
+
from tree_sitter import Node
|
|
61
|
+
|
|
62
|
+
from adl_drift.graph.cfg import CFGBlock, CFGBuilderBase, CFGDef, CFGUse, _node_pos, _text
|
|
63
|
+
from adl_drift.lang.base import CFGConfig
|
|
64
|
+
from adl_drift.lang.java import type_provenance as _tp
|
|
65
|
+
from adl_drift.lang.java.scopes import TYPE_DECLARATIONS
|
|
66
|
+
from adl_drift.lang.java.type_provenance import JavaTypeContext, Provenance, build_type_context
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"TAINT_SOURCES",
|
|
70
|
+
"TAINT_SOURCE_METHODS",
|
|
71
|
+
"TAINT_SOURCE_PARAM_ANNOTATIONS",
|
|
72
|
+
"SINK_METHODS",
|
|
73
|
+
"SINK_METHOD_ARGUMENTS",
|
|
74
|
+
"SINK_CONSTRUCTORS",
|
|
75
|
+
"SINK_CONSTRUCTOR_ARGUMENTS",
|
|
76
|
+
"JavaCFGBuilder",
|
|
77
|
+
"default_java_cfg_config",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# ─── taint sources (codegraph-java-deep-dive.md §10.3) ────────────────────────
|
|
81
|
+
|
|
82
|
+
# Exact dotted-callable text, prefix-matched against a def/argument's full RHS
|
|
83
|
+
# text — same mechanism as Python's TAINT_SOURCES (lang/python/cfg.py).
|
|
84
|
+
TAINT_SOURCES: Tuple[str, ...] = (
|
|
85
|
+
"System.getenv",
|
|
86
|
+
"System.getProperty",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Owner FQN for each `TAINT_SOURCES` entry — a project can declare its own
|
|
90
|
+
# top-level class named `System` in the same file/package, which *does*
|
|
91
|
+
# shadow `java.lang.System`'s implicit import for unqualified references
|
|
92
|
+
# within it (F-04, PROBLEMS.md P4.6); an exact dotted-text match alone can't
|
|
93
|
+
# tell the two apart.
|
|
94
|
+
TAINT_SOURCE_OWNERS: Dict[str, str] = {
|
|
95
|
+
"System.getenv": "java.lang.System",
|
|
96
|
+
"System.getProperty": "java.lang.System",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Final call-target identifier (any receiver) -> source label. Receiver-agnostic
|
|
100
|
+
# because an `HttpServletRequest`/JAX-RS `UriInfo` variable can be named
|
|
101
|
+
# anything, unlike Python's conventionally-named `request` object.
|
|
102
|
+
TAINT_SOURCE_METHODS: Dict[str, str] = {
|
|
103
|
+
"getParameter": "HttpServletRequest.getParameter",
|
|
104
|
+
"getHeader": "HttpServletRequest.getHeader",
|
|
105
|
+
"getQueryString": "HttpServletRequest.getQueryString",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Expected declaring-type FQN(s) for each `TAINT_SOURCE_METHODS` entry (F-04,
|
|
109
|
+
# PROBLEMS.md P4.6) — confirms the call's receiver is plausibly one of these
|
|
110
|
+
# types (via `type_provenance`: declared-variable type + imports) before
|
|
111
|
+
# trusting the simple-name match, so e.g. `Custom.getParameter()` on an
|
|
112
|
+
# unrelated class isn't mistaken for `HttpServletRequest.getParameter`. A
|
|
113
|
+
# method whose receiver can't be confirmed one way or the other (no `import`
|
|
114
|
+
# to check against, or no declared type known for it) still matches — just
|
|
115
|
+
# at a lower confidence (`_owner_provenance` returns `"ambiguous"`) rather
|
|
116
|
+
# than being dropped, to keep recall on unresolved receivers.
|
|
117
|
+
TAINT_SOURCE_METHOD_OWNERS: Dict[str, Tuple[str, ...]] = {
|
|
118
|
+
"getParameter": ("javax.servlet.http.HttpServletRequest", "jakarta.servlet.http.HttpServletRequest"),
|
|
119
|
+
"getHeader": ("javax.servlet.http.HttpServletRequest", "jakarta.servlet.http.HttpServletRequest"),
|
|
120
|
+
"getQueryString": (
|
|
121
|
+
"javax.servlet.http.HttpServletRequest", "jakarta.servlet.http.HttpServletRequest",
|
|
122
|
+
"javax.ws.rs.core.UriInfo", "jakarta.ws.rs.core.UriInfo",
|
|
123
|
+
),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Annotation simple name on a formal parameter -> source label. These mark a
|
|
127
|
+
# parameter as tainted at function entry regardless of caller (Spring MVC /
|
|
128
|
+
# JAX-RS request binding), unlike an ordinary parameter which only becomes
|
|
129
|
+
# tainted via cross-function propagation.
|
|
130
|
+
TAINT_SOURCE_PARAM_ANNOTATIONS: Dict[str, str] = {
|
|
131
|
+
"RequestParam": "@RequestParam",
|
|
132
|
+
"PathVariable": "@PathVariable",
|
|
133
|
+
"RequestBody": "@RequestBody",
|
|
134
|
+
"QueryParam": "@QueryParam",
|
|
135
|
+
"PathParam": "@PathParam",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Expected FQN(s) for each `TAINT_SOURCE_PARAM_ANNOTATIONS` entry — same
|
|
139
|
+
# provenance role as `TAINT_SOURCE_METHOD_OWNERS`, but for annotations: a
|
|
140
|
+
# project-local `@interface RequestParam {}` (or an import of a *different*
|
|
141
|
+
# `RequestParam`) must not be treated as Spring's.
|
|
142
|
+
TAINT_SOURCE_PARAM_ANNOTATION_OWNERS: Dict[str, Tuple[str, ...]] = {
|
|
143
|
+
"RequestParam": ("org.springframework.web.bind.annotation.RequestParam",),
|
|
144
|
+
"PathVariable": ("org.springframework.web.bind.annotation.PathVariable",),
|
|
145
|
+
"RequestBody": ("org.springframework.web.bind.annotation.RequestBody",),
|
|
146
|
+
"QueryParam": ("javax.ws.rs.QueryParam", "jakarta.ws.rs.QueryParam"),
|
|
147
|
+
"PathParam": ("javax.ws.rs.PathParam", "jakarta.ws.rs.PathParam"),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ─── taint sinks (codegraph-java-deep-dive.md §10.4 / PROBLEMS.md P4.6) ──────
|
|
151
|
+
|
|
152
|
+
# Final call-target identifier (any receiver) -> vulnerability category.
|
|
153
|
+
SINK_METHODS: Dict[str, str] = {
|
|
154
|
+
"executeQuery": "sql_injection",
|
|
155
|
+
"executeUpdate": "sql_injection",
|
|
156
|
+
"execute": "sql_injection",
|
|
157
|
+
"createNativeQuery": "sql_injection",
|
|
158
|
+
# `Connection.prepareStatement(sql)` is the actual injection point for the
|
|
159
|
+
# most common JDBC pattern (`PreparedStatement ps = conn.prepareStatement(
|
|
160
|
+
# taintedSql); ps.executeQuery();`) — that `executeQuery()` call itself
|
|
161
|
+
# takes no argument once parameterized, so without this entry the tainted
|
|
162
|
+
# SQL text passed to `prepareStatement` was never checked against any sink
|
|
163
|
+
# at all (regression fix, PROBLEMS.md P4.6). A tainted SQL *template* is
|
|
164
|
+
# still flagged; a bind value passed only through `setString`/`setInt`/...
|
|
165
|
+
# is not a sink argument and stays unflagged (§10.4's own "prepared
|
|
166
|
+
# statements are not automatically safe" callout, "bound parameter"-safe
|
|
167
|
+
# half is preserved unchanged since those setters aren't in this table).
|
|
168
|
+
"prepareStatement": "sql_injection",
|
|
169
|
+
"readObject": "unsafe_deserialization",
|
|
170
|
+
"eval": "code_injection",
|
|
171
|
+
"exec": "command_injection",
|
|
172
|
+
"command": "command_injection",
|
|
173
|
+
"invoke": "reflection_injection",
|
|
174
|
+
"forName": "reflection_injection",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Expected declaring-type FQN(s) for each `SINK_METHODS` entry — same
|
|
178
|
+
# provenance role as `TAINT_SOURCE_METHOD_OWNERS`, so a business method
|
|
179
|
+
# named `execute`/`invoke`/`exec` on a project-owned class isn't mistaken
|
|
180
|
+
# for the real JDK/framework sink.
|
|
181
|
+
SINK_METHOD_OWNERS: Dict[str, Tuple[str, ...]] = {
|
|
182
|
+
"executeQuery": ("java.sql.Statement", "java.sql.PreparedStatement", "java.sql.CallableStatement"),
|
|
183
|
+
"executeUpdate": ("java.sql.Statement", "java.sql.PreparedStatement", "java.sql.CallableStatement"),
|
|
184
|
+
"execute": ("java.sql.Statement", "java.sql.PreparedStatement", "java.sql.CallableStatement"),
|
|
185
|
+
"createNativeQuery": ("jakarta.persistence.EntityManager", "javax.persistence.EntityManager"),
|
|
186
|
+
"prepareStatement": ("java.sql.Connection",),
|
|
187
|
+
"readObject": ("java.io.ObjectInputStream",),
|
|
188
|
+
"eval": ("javax.script.ScriptEngine",),
|
|
189
|
+
"exec": ("java.lang.Runtime",),
|
|
190
|
+
"command": ("java.lang.ProcessBuilder",),
|
|
191
|
+
"invoke": ("java.lang.reflect.Method",),
|
|
192
|
+
"forName": ("java.lang.Class",),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
SinkArgumentPolicy = Union[Tuple[int, ...], Literal["all", "receiver"]]
|
|
196
|
+
|
|
197
|
+
SINK_METHOD_ARGUMENTS: Dict[str, SinkArgumentPolicy] = {
|
|
198
|
+
"prepareStatement": (0,),
|
|
199
|
+
"createNativeQuery": (0,),
|
|
200
|
+
"executeQuery": (0,),
|
|
201
|
+
"executeUpdate": (0,),
|
|
202
|
+
"execute": (0,),
|
|
203
|
+
# `readObject()` takes no argument at all — the JDK API is
|
|
204
|
+
# `Object ObjectInputStream.readObject()` — so the attacker-controlled
|
|
205
|
+
# data is the *receiver* (the stream itself), not a positional argument.
|
|
206
|
+
# A positional-args policy ("all"/index tuple) always yields an empty
|
|
207
|
+
# list for this call shape, which `_sink_info` (lang/java/taint.py)
|
|
208
|
+
# then treats as "not a sink" and drops before taint is ever checked
|
|
209
|
+
# (PROBLEMS.md P4.6 F-01 regression) — "receiver" tells `_sink_args` to
|
|
210
|
+
# check the receiver expression for taint instead of any argument.
|
|
211
|
+
"readObject": "receiver",
|
|
212
|
+
"eval": (0,),
|
|
213
|
+
"exec": "all",
|
|
214
|
+
"command": "all",
|
|
215
|
+
"invoke": "all",
|
|
216
|
+
"forName": (0,),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
SINK_METHOD_EXTERNAL_SUBTYPE_FALLBACKS: frozenset[str] = frozenset({
|
|
220
|
+
"prepareStatement",
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
# Constructed type simple name (`new X(...)`) -> category.
|
|
224
|
+
SINK_CONSTRUCTORS: Dict[str, str] = {
|
|
225
|
+
"ProcessBuilder": "command_injection",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
SINK_CONSTRUCTOR_OWNERS: Dict[str, Tuple[str, ...]] = {
|
|
229
|
+
"ProcessBuilder": ("java.lang.ProcessBuilder",),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
SINK_CONSTRUCTOR_ARGUMENTS: Dict[str, SinkArgumentPolicy] = {
|
|
233
|
+
"ProcessBuilder": "all",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# ─── literal / structural node-type tables ────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
_LITERAL_TYPES: frozenset = frozenset({
|
|
239
|
+
"decimal_integer_literal", "hex_integer_literal", "octal_integer_literal",
|
|
240
|
+
"binary_integer_literal", "decimal_floating_point_literal",
|
|
241
|
+
"hex_floating_point_literal", "true", "false", "character_literal",
|
|
242
|
+
"string_literal", "null_literal", "text_block",
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
_TERMINATOR_TYPES: frozenset = frozenset({
|
|
246
|
+
"return_statement", "throw_statement", "break_statement", "continue_statement",
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
_SIMPLE_STMT_TYPES: frozenset = frozenset({
|
|
250
|
+
"import_declaration", "package_declaration", "empty_statement", "yield_statement",
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
_METHOD_TYPES = ("method_declaration", "constructor_declaration", "compact_constructor_declaration")
|
|
254
|
+
|
|
255
|
+
_TYPE_DECL_TYPES = (
|
|
256
|
+
"class_declaration", "interface_declaration", "enum_declaration",
|
|
257
|
+
"record_declaration", "annotation_type_declaration",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ─── helper functions (Java-CST-coupled) ──────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
def _real_children(node: Node) -> List[Node]:
|
|
264
|
+
"""Non-extra, non-punctuation children of a `block`/`switch_block`-like node."""
|
|
265
|
+
return [c for c in node.children
|
|
266
|
+
if not c.is_extra and c.type not in ("{", "}", ";", ",")]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _body_stmts(node: Optional[Node]) -> List[Node]:
|
|
270
|
+
"""Statement list for a compound-statement body, whether braced
|
|
271
|
+
(`block`) or a single braceless statement (`if (x) foo();` is legal
|
|
272
|
+
Java)."""
|
|
273
|
+
if node is None:
|
|
274
|
+
return []
|
|
275
|
+
if node.type == "block":
|
|
276
|
+
return _real_children(node)
|
|
277
|
+
return [node]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _fields(node: Node, field_name: str) -> List[Node]:
|
|
281
|
+
"""Every child of `node` bound to `field_name` (`child_by_field_name`
|
|
282
|
+
only returns the first — `int a = 1, b = 2;` has two `declarator`
|
|
283
|
+
fields, `for (int i=0,j=0;;i++,j++)` has two `init`/`update` fields)."""
|
|
284
|
+
out: List[Node] = []
|
|
285
|
+
for i, child in enumerate(node.children):
|
|
286
|
+
if node.field_name_for_child(i) == field_name:
|
|
287
|
+
out.append(child)
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _first_named_child(node: Node) -> Optional[Node]:
|
|
292
|
+
for child in node.children:
|
|
293
|
+
if child.is_named:
|
|
294
|
+
return child
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _is_literal(node: Optional[Node]) -> bool:
|
|
299
|
+
"""True when node represents a compile-time constant (Java literals)."""
|
|
300
|
+
if node is None:
|
|
301
|
+
return False
|
|
302
|
+
if node.type in _LITERAL_TYPES:
|
|
303
|
+
return True
|
|
304
|
+
if node.type == "unary_expression":
|
|
305
|
+
op = node.child_by_field_name("operator")
|
|
306
|
+
operand = node.child_by_field_name("operand")
|
|
307
|
+
if op is not None and op.type == "-" and operand is not None and operand.type in (
|
|
308
|
+
"decimal_integer_literal", "hex_integer_literal", "octal_integer_literal",
|
|
309
|
+
"binary_integer_literal", "decimal_floating_point_literal",
|
|
310
|
+
"hex_floating_point_literal",
|
|
311
|
+
):
|
|
312
|
+
return True
|
|
313
|
+
if node.type == "parenthesized_expression" and node.child_count >= 3:
|
|
314
|
+
return _is_literal(node.children[1])
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _uses_of(node: Optional[Node]) -> List[CFGUse]:
|
|
319
|
+
out: List[CFGUse] = []
|
|
320
|
+
_collect_uses(node, out)
|
|
321
|
+
return out
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _collect_uses(node: Optional[Node], uses: List[CFGUse], _visited: Optional[set] = None) -> None:
|
|
325
|
+
"""Walk *node* and append identifier reads to *uses*.
|
|
326
|
+
|
|
327
|
+
Skips:
|
|
328
|
+
- Method/field names (`obj.method(...)`/`obj.field` — only `obj` and
|
|
329
|
+
the arguments are reads)
|
|
330
|
+
- Constructed type names (`new X(...)` — only the arguments are reads)
|
|
331
|
+
- Type positions (`(Foo) x` cast, `x instanceof Foo`, declared types)
|
|
332
|
+
- Annotations
|
|
333
|
+
- Lambda bodies (opaque, matching Python's nested-def treatment)
|
|
334
|
+
"""
|
|
335
|
+
if node is None:
|
|
336
|
+
return
|
|
337
|
+
if _visited is None:
|
|
338
|
+
_visited = set()
|
|
339
|
+
nid = id(node)
|
|
340
|
+
if nid in _visited:
|
|
341
|
+
return
|
|
342
|
+
_visited.add(nid)
|
|
343
|
+
|
|
344
|
+
t = node.type
|
|
345
|
+
|
|
346
|
+
if t == "field_access":
|
|
347
|
+
obj = node.child_by_field_name("object")
|
|
348
|
+
_collect_uses(obj, uses, _visited)
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
if t == "method_invocation":
|
|
352
|
+
obj = node.child_by_field_name("object")
|
|
353
|
+
args = node.child_by_field_name("arguments")
|
|
354
|
+
_collect_uses(obj, uses, _visited)
|
|
355
|
+
_collect_uses(args, uses, _visited)
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
if t == "object_creation_expression":
|
|
359
|
+
args = node.child_by_field_name("arguments")
|
|
360
|
+
_collect_uses(args, uses, _visited)
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
if t == "cast_expression":
|
|
364
|
+
_collect_uses(node.child_by_field_name("value"), uses, _visited)
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
if t == "instanceof_expression":
|
|
368
|
+
_collect_uses(node.child_by_field_name("left"), uses, _visited)
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
if t in ("annotation", "marker_annotation"):
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
if t == "lambda_expression":
|
|
375
|
+
# Opaque value — don't descend, matching Python's lambda handling.
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
if t == "this":
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
if t == "identifier":
|
|
382
|
+
txt = _text(node, b"") if node.text is None else (
|
|
383
|
+
node.text.decode("utf-8", errors="replace")
|
|
384
|
+
)
|
|
385
|
+
if txt:
|
|
386
|
+
uses.append(CFGUse(txt, _node_pos(node)))
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
for child in node.children:
|
|
390
|
+
_collect_uses(child, uses, _visited)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _annotations_of(node: Node, source: bytes) -> List[Tuple[str, int]]:
|
|
394
|
+
"""`[(raw_name_text, start_byte), ...]` for every annotation on a
|
|
395
|
+
`formal_parameter`/method/field's `modifiers` child. *raw_name_text*
|
|
396
|
+
keeps the full dotted path exactly as written (`@RequestParam` ->
|
|
397
|
+
`"RequestParam"`, `@org.springframework...RequestParam` -> the whole
|
|
398
|
+
FQN text) — trimming to a simple name here would throw away exactly
|
|
399
|
+
what `type_provenance.annotation_provenance` needs to tell a
|
|
400
|
+
fully-qualified annotation from a simple one that needs an `import` to
|
|
401
|
+
confirm (F-04, PROBLEMS.md P4.6)."""
|
|
402
|
+
modifiers = next((c for c in node.children if c.type == "modifiers"), None)
|
|
403
|
+
if modifiers is None:
|
|
404
|
+
return []
|
|
405
|
+
out: List[Tuple[str, int]] = []
|
|
406
|
+
for child in modifiers.children:
|
|
407
|
+
if child.type not in ("annotation", "marker_annotation"):
|
|
408
|
+
continue
|
|
409
|
+
name_node = child.child_by_field_name("name")
|
|
410
|
+
if name_node is not None:
|
|
411
|
+
out.append((_text(name_node, source).lstrip("@"), child.start_byte))
|
|
412
|
+
return out
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _annotation_owner_verdict(
|
|
416
|
+
raw_name: str, at_byte: int, owners: Optional[Tuple[str, ...]], ctx: Optional[JavaTypeContext],
|
|
417
|
+
) -> Provenance:
|
|
418
|
+
"""`"confirmed"`/`"ambiguous"`/`"rejected"` for whether *raw_name* (an
|
|
419
|
+
annotation as written — possibly fully qualified) denotes one of
|
|
420
|
+
*owners*. No `owners` entry, or no `ctx` (no file-level import data
|
|
421
|
+
available), means "nothing to check against" -> `"ambiguous"` — the
|
|
422
|
+
caller should still treat it as a match, just at reduced confidence."""
|
|
423
|
+
if not owners or ctx is None:
|
|
424
|
+
return "ambiguous"
|
|
425
|
+
verdicts = [
|
|
426
|
+
_tp.annotation_provenance(raw_name, at_byte, ctx.imports, ctx.local_types, fqn)
|
|
427
|
+
for fqn in owners
|
|
428
|
+
]
|
|
429
|
+
if any(v == "confirmed" for v in verdicts):
|
|
430
|
+
return "confirmed"
|
|
431
|
+
if all(v == "rejected" for v in verdicts):
|
|
432
|
+
return "rejected"
|
|
433
|
+
return "ambiguous"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _erased_type_name(type_text: str) -> str:
|
|
437
|
+
"""Strip generic arguments/array brackets for a constructed-type match
|
|
438
|
+
(`new ProcessBuilder<>(...)`-shaped edge cases, generics on a raw type
|
|
439
|
+
are not legal Java for `new`, but this keeps the match robust to
|
|
440
|
+
whitespace/array suffixes)."""
|
|
441
|
+
text = type_text.strip()
|
|
442
|
+
idx = text.find("<")
|
|
443
|
+
if idx != -1:
|
|
444
|
+
text = text[:idx]
|
|
445
|
+
return text.strip()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _constructed_receiver_type(obj: Optional[Node], source: bytes) -> str:
|
|
449
|
+
"""Simple type name of an inline `new X(...)` receiver
|
|
450
|
+
(`new ObjectInputStream(...).readObject()`), F-07 (PROBLEMS.md P4.6).
|
|
451
|
+
Such a receiver has no declared-variable type and its own text isn't
|
|
452
|
+
identifier-shaped (it's a whole constructor call, not a name), so
|
|
453
|
+
neither of `_receiver_type_candidates`' existing candidate paths ever
|
|
454
|
+
recognized it: `_owner_provenance` saw zero candidates and fell back to
|
|
455
|
+
`"ambiguous"` even when the constructed type is spelled right there in
|
|
456
|
+
the source and exactly matches the sink's expected owner — silently
|
|
457
|
+
downgrading a fully-confirmable sink to reduced confidence."""
|
|
458
|
+
if obj is None or obj.type != "object_creation_expression":
|
|
459
|
+
return ""
|
|
460
|
+
type_node = obj.child_by_field_name("type")
|
|
461
|
+
if type_node is None:
|
|
462
|
+
return ""
|
|
463
|
+
return _erased_type_name(_text(type_node, source))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _enclosing_type_name(node: Node, source: bytes) -> str:
|
|
467
|
+
"""Simple name of the innermost type declaration lexically enclosing
|
|
468
|
+
*node* — the implicit `this` receiver for an *unqualified* call (no
|
|
469
|
+
`object` field at all: `getParameter("cmd")`, `executeQuery(q)`), F-08
|
|
470
|
+
(PROBLEMS.md P4.6). Such a call can only resolve to a method declared on
|
|
471
|
+
`this` or inherited by it, so the type to confirm against a sink/
|
|
472
|
+
source's expected owner is the enclosing class itself — not an absent
|
|
473
|
+
receiver, which previously gave `_receiver_type_candidates` zero
|
|
474
|
+
candidates and made `_owner_provenance` fall back to `"ambiguous"`
|
|
475
|
+
(still matched, just at reduced confidence) for *any* unqualified call,
|
|
476
|
+
including one to a plain, unrelated local method that merely happens to
|
|
477
|
+
share a sink/source method's simple name."""
|
|
478
|
+
parent = node.parent
|
|
479
|
+
while parent is not None:
|
|
480
|
+
if parent.type in TYPE_DECLARATIONS:
|
|
481
|
+
return _text(parent.child_by_field_name("name"), source)
|
|
482
|
+
parent = parent.parent
|
|
483
|
+
return ""
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _receiver_type_candidates(
|
|
487
|
+
receiver: str, ctx: Optional[JavaTypeContext], at_byte: int,
|
|
488
|
+
constructed_type: str = "", enclosing_type: str = "",
|
|
489
|
+
) -> Tuple[str, ...]:
|
|
490
|
+
"""Type-name-shaped candidates for a call's receiver expression, for
|
|
491
|
+
`_owner_provenance`. A bare receiver *identifier* (`request`, `custom`)
|
|
492
|
+
with a known *declared* variable type uses that type as its candidate.
|
|
493
|
+
|
|
494
|
+
Two `Type.staticMethod()`-shaped exceptions, where the receiver text
|
|
495
|
+
itself already spells a type rather than naming a variable:
|
|
496
|
+
- `Runtime.getRuntime()` specifically (`security.py`'s own precedent).
|
|
497
|
+
- Any other bare identifier with *no* declared-variable type that
|
|
498
|
+
looks like a Java type name — starts uppercase, per the PascalCase-
|
|
499
|
+
types/camelCase-variables convention (`Custom.forName(...)`) — F-04,
|
|
500
|
+
PROBLEMS.md P4.6. `type_provenance` naturally rejects a candidate
|
|
501
|
+
that plainly isn't the expected simple name, so offering it
|
|
502
|
+
alongside a genuine declared-variable-type candidate is safe: at
|
|
503
|
+
most one of them can ever match a given owner.
|
|
504
|
+
|
|
505
|
+
A third exception, `constructed_type` (`_constructed_receiver_type`):
|
|
506
|
+
an inline `new X(...)` receiver's own constructed type name — F-07,
|
|
507
|
+
PROBLEMS.md P4.6.
|
|
508
|
+
|
|
509
|
+
A fourth, `enclosing_type` (`_enclosing_type_name`): the implicit `this`
|
|
510
|
+
for a call with no receiver at all — F-08, PROBLEMS.md P4.6. Unlike the
|
|
511
|
+
PascalCase-guess exception above, this is never a guess: the caller only
|
|
512
|
+
passes it once it has actually found the enclosing type declaration."""
|
|
513
|
+
candidates = []
|
|
514
|
+
declared = ""
|
|
515
|
+
if ctx is not None:
|
|
516
|
+
declared = _tp.declared_type_of(receiver, ctx.declared_types, at_byte)
|
|
517
|
+
if declared:
|
|
518
|
+
candidates.append(declared)
|
|
519
|
+
if constructed_type:
|
|
520
|
+
candidates.append(constructed_type)
|
|
521
|
+
if enclosing_type:
|
|
522
|
+
candidates.append(enclosing_type)
|
|
523
|
+
if receiver.endswith(".getRuntime()"):
|
|
524
|
+
candidates.append(receiver[: -len(".getRuntime()")])
|
|
525
|
+
elif not declared and receiver[:1].isupper() and receiver.isidentifier():
|
|
526
|
+
candidates.append(receiver)
|
|
527
|
+
return tuple(candidates)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _owner_name_signal(simple: str, owners: Tuple[str, ...]) -> bool:
|
|
531
|
+
"""True when *simple* (an external, hierarchy-unknown candidate type's
|
|
532
|
+
own simple name) textually suggests one of *owners* — e.g.
|
|
533
|
+
`OracleConnection` for owner `java.sql.Connection` (owner simple name
|
|
534
|
+
`Connection`). Gates `_owner_provenance`'s external-subtype fallback:
|
|
535
|
+
an imported type is only a *plausible* undeclared vendor subtype when
|
|
536
|
+
its own name carries some signal toward the expected owner, not merely
|
|
537
|
+
because it happens to have been imported at all (PROBLEMS.md P4.6 —
|
|
538
|
+
2026-08-28 regression: `import com.example.QueryBuilder; ...
|
|
539
|
+
builder.prepareStatement(sql)` was flagged `sql_injection` purely
|
|
540
|
+
because `QueryBuilder` was an explicit import, with zero evidence it
|
|
541
|
+
has anything to do with `java.sql.Connection`)."""
|
|
542
|
+
simple_lower = simple.lower()
|
|
543
|
+
for owner in owners:
|
|
544
|
+
owner_simple = owner.rsplit(".", 1)[-1].lower()
|
|
545
|
+
if owner_simple and owner_simple in simple_lower:
|
|
546
|
+
return True
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _owner_provenance(
|
|
551
|
+
candidates: Tuple[str, ...], at_byte: int, owners: Optional[Tuple[str, ...]],
|
|
552
|
+
ctx: Optional[JavaTypeContext], *, allow_external_subtype_fallback: bool = False,
|
|
553
|
+
) -> Provenance:
|
|
554
|
+
"""`"confirmed"`/`"ambiguous"`/`"rejected"` for whether any of
|
|
555
|
+
*candidates* (a call's receiver text, its declared variable type, ...)
|
|
556
|
+
plausibly denotes one of *owners*. No `owners` entry for this call name,
|
|
557
|
+
or no `ctx`, means "nothing to check against" -> `"ambiguous"`."""
|
|
558
|
+
if not owners or ctx is None:
|
|
559
|
+
return "ambiguous"
|
|
560
|
+
verdicts = [
|
|
561
|
+
_tp.assignability_provenance(cand, ctx.imports, ctx.local_types, ctx.local_supertypes, fqn, at_byte)
|
|
562
|
+
for cand in candidates if cand
|
|
563
|
+
for fqn in owners
|
|
564
|
+
]
|
|
565
|
+
if any(v == "confirmed" for v in verdicts):
|
|
566
|
+
return "confirmed"
|
|
567
|
+
if allow_external_subtype_fallback and verdicts and all(v == "rejected" for v in verdicts):
|
|
568
|
+
for cand in candidates:
|
|
569
|
+
simple = _tp.simple_type(cand)
|
|
570
|
+
if simple and not _tp.type_declared_at(ctx.local_types, simple, at_byte):
|
|
571
|
+
if _owner_name_signal(simple, owners) and any(
|
|
572
|
+
imported.endswith(f".{simple}") for imported in ctx.imports
|
|
573
|
+
):
|
|
574
|
+
return "ambiguous"
|
|
575
|
+
if verdicts and all(v == "rejected" for v in verdicts):
|
|
576
|
+
return "rejected"
|
|
577
|
+
return "ambiguous"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _source_match_for(
|
|
581
|
+
node: Optional[Node], config: CFGConfig, source: bytes, ctx: Optional[JavaTypeContext] = None,
|
|
582
|
+
) -> Optional[Tuple[str, bool]]:
|
|
583
|
+
"""Find a taint-source invocation anywhere inside *node*, not just at its
|
|
584
|
+
root — descends through binary concatenation, parenthesized/ternary/cast
|
|
585
|
+
expressions, nested call receivers/arguments, and array-literal elements
|
|
586
|
+
so a source doesn't have to be the whole RHS to be recognized (`"sh -c "
|
|
587
|
+
+ System.getenv("CMD")`, `wrap(System.getenv("D"))`, `cond ?
|
|
588
|
+
System.getenv("A") : "b"`, `new String[] { "sh", System.getenv("D") }`
|
|
589
|
+
— F-09, PROBLEMS.md P4.6 — ...).
|
|
590
|
+
|
|
591
|
+
Returns `(label, confirmed)`: `confirmed` is `True` when the receiver's
|
|
592
|
+
declared type was positively matched against `TAINT_SOURCE_METHOD_
|
|
593
|
+
OWNERS` (F-04, PROBLEMS.md P4.6) — e.g. `Custom.getParameter()` on a
|
|
594
|
+
class unrelated to `HttpServletRequest` is *not* returned at all (its
|
|
595
|
+
receiver type is confirmed to be something else), whereas a receiver
|
|
596
|
+
with no resolvable declared type still matches, just as `confirmed=
|
|
597
|
+
False`, to keep recall on an unresolved receiver. A dotted `TAINT_
|
|
598
|
+
SOURCES` entry (`System.getenv`) is matched against the call's own
|
|
599
|
+
`object.name` text exactly, not as a text prefix — `System.getenvFake(
|
|
600
|
+
...)` must not match `System.getenv`."""
|
|
601
|
+
if node is None:
|
|
602
|
+
return None
|
|
603
|
+
t = node.type
|
|
604
|
+
|
|
605
|
+
if t == "method_invocation":
|
|
606
|
+
obj = node.child_by_field_name("object")
|
|
607
|
+
name_node = node.child_by_field_name("name")
|
|
608
|
+
if obj is not None and name_node is not None:
|
|
609
|
+
obj_text = _text(obj, source)
|
|
610
|
+
method_name = _text(name_node, source)
|
|
611
|
+
dotted = f"{obj_text}.{method_name}"
|
|
612
|
+
src_key = dotted if dotted in config.taint_sources else None
|
|
613
|
+
if src_key is None:
|
|
614
|
+
# Fully-qualified receiver (`java.lang.System.getenv(...)`),
|
|
615
|
+
# not just the short `System.getenv` form the dotted-text
|
|
616
|
+
# equality above catches — F-04 gap (PROBLEMS.md P4.6):
|
|
617
|
+
# `dotted` above never equals the short `TAINT_SOURCES` entry
|
|
618
|
+
# for an FQN call, so it fell through unmatched even though
|
|
619
|
+
# `_owner_provenance`/`type_provenance` already confirm an
|
|
620
|
+
# exact FQN receiver text on their own (`raw_type ==
|
|
621
|
+
# qualified_name` short-circuit). Only trusted when
|
|
622
|
+
# `obj_text` equals the *entire* owner FQN, not a suffix of
|
|
623
|
+
# it, so this can't be fooled by an unrelated `x.y.System`.
|
|
624
|
+
for cand_key in config.taint_sources:
|
|
625
|
+
cand_method = cand_key.rsplit(".", 1)[-1]
|
|
626
|
+
owner = TAINT_SOURCE_OWNERS.get(cand_key)
|
|
627
|
+
if owner and method_name == cand_method and obj_text == owner:
|
|
628
|
+
src_key = cand_key
|
|
629
|
+
break
|
|
630
|
+
if src_key is not None:
|
|
631
|
+
owner = TAINT_SOURCE_OWNERS.get(src_key)
|
|
632
|
+
verdict = _owner_provenance(
|
|
633
|
+
(obj_text,), node.start_byte, (owner,) if owner else None, ctx,
|
|
634
|
+
)
|
|
635
|
+
if verdict != "rejected":
|
|
636
|
+
return src_key, verdict == "confirmed"
|
|
637
|
+
if name_node is not None:
|
|
638
|
+
method_name = _text(name_node, source)
|
|
639
|
+
label = config.taint_source_methods.get(method_name)
|
|
640
|
+
if label:
|
|
641
|
+
receiver = _text(obj, source) if obj is not None else ""
|
|
642
|
+
verdict = _owner_provenance(
|
|
643
|
+
_receiver_type_candidates(
|
|
644
|
+
receiver, ctx, node.start_byte, _constructed_receiver_type(obj, source),
|
|
645
|
+
_enclosing_type_name(node, source) if obj is None else "",
|
|
646
|
+
),
|
|
647
|
+
node.start_byte, TAINT_SOURCE_METHOD_OWNERS.get(method_name), ctx,
|
|
648
|
+
)
|
|
649
|
+
if verdict != "rejected":
|
|
650
|
+
return label, verdict == "confirmed"
|
|
651
|
+
found = _source_match_for(obj, config, source, ctx)
|
|
652
|
+
if found:
|
|
653
|
+
return found
|
|
654
|
+
return _source_match_for(node.child_by_field_name("arguments"), config, source, ctx)
|
|
655
|
+
|
|
656
|
+
if t == "binary_expression":
|
|
657
|
+
return (
|
|
658
|
+
_source_match_for(node.child_by_field_name("left"), config, source, ctx)
|
|
659
|
+
or _source_match_for(node.child_by_field_name("right"), config, source, ctx)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
if t == "ternary_expression":
|
|
663
|
+
return (
|
|
664
|
+
_source_match_for(node.child_by_field_name("consequence"), config, source, ctx)
|
|
665
|
+
or _source_match_for(node.child_by_field_name("alternative"), config, source, ctx)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if t == "parenthesized_expression":
|
|
669
|
+
return _source_match_for(_first_named_child(node), config, source, ctx)
|
|
670
|
+
|
|
671
|
+
if t == "cast_expression":
|
|
672
|
+
return _source_match_for(node.child_by_field_name("value"), config, source, ctx)
|
|
673
|
+
|
|
674
|
+
if t == "object_creation_expression":
|
|
675
|
+
# A source nested inside an inline `new X(...)` receiver/argument
|
|
676
|
+
# (`new ObjectInputStream(new FileInputStream(System.getenv("P")))`)
|
|
677
|
+
# was previously invisible (F-06, PROBLEMS.md P4.6): this function
|
|
678
|
+
# recursed into a call's `object`/`arguments` but had no branch for a
|
|
679
|
+
# constructor call at all, so it fell through to the catch-all
|
|
680
|
+
# `return None` below without ever inspecting the constructor's own
|
|
681
|
+
# arguments. Only matters when the source isn't already bound to a
|
|
682
|
+
# tainted variable first (the common case, `_ids_in`/the tainted-set
|
|
683
|
+
# walk, already covers that) — an inline constructed
|
|
684
|
+
# receiver/argument has no such binding to fall back on.
|
|
685
|
+
return _source_match_for(node.child_by_field_name("arguments"), config, source, ctx)
|
|
686
|
+
|
|
687
|
+
if t == "array_creation_expression":
|
|
688
|
+
# A source nested inside an inline array literal argument (`new
|
|
689
|
+
# ProcessBuilder(new String[] { "sh", "-c", System.getenv("CMD") })`)
|
|
690
|
+
# was previously invisible (F-09, PROBLEMS.md P4.6) for the same
|
|
691
|
+
# reason as F-06 above: no branch here for an array-creation node, so
|
|
692
|
+
# it fell through to `return None` without ever inspecting the
|
|
693
|
+
# array's elements.
|
|
694
|
+
return _source_match_for(node.child_by_field_name("value"), config, source, ctx)
|
|
695
|
+
|
|
696
|
+
if t in ("argument_list", "array_initializer"):
|
|
697
|
+
for child in node.children:
|
|
698
|
+
if child.is_named:
|
|
699
|
+
found = _source_match_for(child, config, source, ctx)
|
|
700
|
+
if found:
|
|
701
|
+
return found
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
if t == "switch_expression":
|
|
705
|
+
# A source nested inside a `switch` expression's arms (`switch (x) {
|
|
706
|
+
# case 1 -> System.getenv("Q"); default -> "static"; }`, Java 14+)
|
|
707
|
+
# was previously invisible (PROBLEMS.md P4.6) for the same reason as
|
|
708
|
+
# F-06/F-09 above: no branch here for `switch_expression`, so it fell
|
|
709
|
+
# through to `return None` without ever inspecting any arm's value.
|
|
710
|
+
body = node.child_by_field_name("body")
|
|
711
|
+
if body is None:
|
|
712
|
+
return None
|
|
713
|
+
for group in body.children:
|
|
714
|
+
if group.type not in ("switch_rule", "switch_block_statement_group"):
|
|
715
|
+
continue
|
|
716
|
+
for stmt in group.children:
|
|
717
|
+
if not stmt.is_named or stmt.type == "switch_label":
|
|
718
|
+
continue
|
|
719
|
+
found = _switch_arm_match(stmt, config, source, ctx)
|
|
720
|
+
if found:
|
|
721
|
+
return found
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
return None
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _switch_arm_match(
|
|
728
|
+
stmt: Node, config: CFGConfig, source: bytes, ctx: Optional[JavaTypeContext] = None,
|
|
729
|
+
) -> Optional[Tuple[str, bool]]:
|
|
730
|
+
"""The value a single `switch_rule`/`switch_block_statement_group`
|
|
731
|
+
statement contributes, if any — `expression_statement` (arrow form,
|
|
732
|
+
`case 1 -> System.getenv("Q");`) and `yield_statement` (block-arrow form
|
|
733
|
+
or classic `case 1: yield ...;` form) each wrap exactly the value
|
|
734
|
+
expression; a `block` arm is searched for its own `yield_statement`(s),
|
|
735
|
+
recursing through `if`/`try`/`synchronized` control structures that can
|
|
736
|
+
still contain one (`case 1 -> { if (cond) { yield System.getenv("Q"); }
|
|
737
|
+
yield "safe"; }` — a real false negative found in review: the taint
|
|
738
|
+
source here was previously invisible because this function stopped at
|
|
739
|
+
`if_statement`/`try_statement`/`synchronized_statement` instead of
|
|
740
|
+
looking inside them for a `yield`, and — contrary to what an earlier
|
|
741
|
+
version of this docstring claimed — `lang/java/taint.py`'s identifier
|
|
742
|
+
propagation does NOT independently discover a *new* source call nested
|
|
743
|
+
this way; it only propagates taint between identifiers already known to
|
|
744
|
+
be tainted). A `throw_statement` arm never completes the switch with a
|
|
745
|
+
value, so it's deliberately not recursed into — matching
|
|
746
|
+
`_source_match_for`'s own conservative-for-recall,
|
|
747
|
+
not-conservative-for-nonsense philosophy would mean flagging code paths
|
|
748
|
+
that can't actually produce the tainted value. A nested
|
|
749
|
+
`switch_expression`/`switch_statement` is also not descended into here:
|
|
750
|
+
a `yield` inside it resolves to that inner switch, not this one, so it's
|
|
751
|
+
left alone (its own value, if used, is picked up wherever it's actually
|
|
752
|
+
consumed by `_source_match_for`)."""
|
|
753
|
+
t = stmt.type
|
|
754
|
+
if t in ("expression_statement", "yield_statement"):
|
|
755
|
+
return _source_match_for(_first_named_child(stmt), config, source, ctx)
|
|
756
|
+
if t == "block":
|
|
757
|
+
for child in stmt.children:
|
|
758
|
+
if child.is_named:
|
|
759
|
+
found = _switch_arm_match(child, config, source, ctx)
|
|
760
|
+
if found:
|
|
761
|
+
return found
|
|
762
|
+
return None
|
|
763
|
+
if t == "if_statement":
|
|
764
|
+
for field_name in ("consequence", "alternative"):
|
|
765
|
+
branch = stmt.child_by_field_name(field_name)
|
|
766
|
+
if branch is not None:
|
|
767
|
+
found = _switch_arm_match(branch, config, source, ctx)
|
|
768
|
+
if found:
|
|
769
|
+
return found
|
|
770
|
+
return None
|
|
771
|
+
if t in ("try_statement", "try_with_resources_statement"):
|
|
772
|
+
body = stmt.child_by_field_name("body")
|
|
773
|
+
if body is not None:
|
|
774
|
+
found = _switch_arm_match(body, config, source, ctx)
|
|
775
|
+
if found:
|
|
776
|
+
return found
|
|
777
|
+
for child in stmt.children:
|
|
778
|
+
if child.type != "catch_clause":
|
|
779
|
+
continue
|
|
780
|
+
catch_body = child.child_by_field_name("body")
|
|
781
|
+
if catch_body is not None:
|
|
782
|
+
found = _switch_arm_match(catch_body, config, source, ctx)
|
|
783
|
+
if found:
|
|
784
|
+
return found
|
|
785
|
+
return None
|
|
786
|
+
if t == "synchronized_statement":
|
|
787
|
+
body = stmt.child_by_field_name("body")
|
|
788
|
+
return _switch_arm_match(body, config, source, ctx) if body is not None else None
|
|
789
|
+
return None
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _source_label_for(
|
|
793
|
+
node: Optional[Node], config: CFGConfig, source: bytes, ctx: Optional[JavaTypeContext] = None,
|
|
794
|
+
) -> Optional[str]:
|
|
795
|
+
"""Label-only convenience wrapper around `_source_match_for` for callers
|
|
796
|
+
that don't need the confirmed/ambiguous distinction."""
|
|
797
|
+
match = _source_match_for(node, config, source, ctx)
|
|
798
|
+
return match[0] if match else None
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _collect_param_specs(
|
|
802
|
+
params_node: Optional[Node], source: bytes,
|
|
803
|
+
) -> List[Tuple[str, List[Tuple[str, int]]]]:
|
|
804
|
+
"""`[(param_name, [(raw_annotation_name, start_byte), ...]), ...]` for a
|
|
805
|
+
`formal_parameters` CST node (handles plain `formal_parameter` and
|
|
806
|
+
varargs `spread_parameter`)."""
|
|
807
|
+
if params_node is None:
|
|
808
|
+
return []
|
|
809
|
+
out: List[Tuple[str, List[Tuple[str, int]]]] = []
|
|
810
|
+
for child in params_node.children:
|
|
811
|
+
if child.type not in ("formal_parameter", "spread_parameter"):
|
|
812
|
+
continue
|
|
813
|
+
name_node = child.child_by_field_name("name")
|
|
814
|
+
if name_node is None:
|
|
815
|
+
continue
|
|
816
|
+
out.append((_text(name_node, source), _annotations_of(child, source)))
|
|
817
|
+
return out
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _method_parameters(node: Node) -> Optional[Node]:
|
|
821
|
+
params = node.child_by_field_name("parameters")
|
|
822
|
+
if params is not None or node.type != "compact_constructor_declaration":
|
|
823
|
+
return params
|
|
824
|
+
record = node.parent.parent if node.parent is not None else None
|
|
825
|
+
if record is not None and record.type == "record_declaration":
|
|
826
|
+
return record.child_by_field_name("parameters")
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# ─── CFG builder ──────────────────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
class JavaCFGBuilder(CFGBuilderBase):
|
|
833
|
+
"""Converts a method/constructor CST node into a basic-block CFG."""
|
|
834
|
+
|
|
835
|
+
def build(
|
|
836
|
+
self, func_node: Node, source: bytes
|
|
837
|
+
) -> Tuple[int, int, Dict[int, CFGBlock], List[CFGDef]]:
|
|
838
|
+
self._reset()
|
|
839
|
+
|
|
840
|
+
# Per-compilation-unit import/declared-type provenance data (F-04,
|
|
841
|
+
# PROBLEMS.md P4.6) — computed once per `build()` call (once per
|
|
842
|
+
# method; this builder isn't on a hot per-file loop, see module
|
|
843
|
+
# docstring) and consulted by `_taint_of`/the annotation loop below
|
|
844
|
+
# to confirm a source/sink match's receiver or annotation actually
|
|
845
|
+
# denotes the real JDK/framework type, not a same-named local one.
|
|
846
|
+
root = func_node
|
|
847
|
+
while root.parent is not None:
|
|
848
|
+
root = root.parent
|
|
849
|
+
self._type_ctx = build_type_context(root, source)
|
|
850
|
+
|
|
851
|
+
entry = self._new_block()
|
|
852
|
+
exit_b = self._new_block()
|
|
853
|
+
|
|
854
|
+
params_node = _method_parameters(func_node)
|
|
855
|
+
for name, annotations in _collect_param_specs(params_node, source):
|
|
856
|
+
taint = None
|
|
857
|
+
for raw_name, at_byte in annotations:
|
|
858
|
+
simple = raw_name.rsplit(".", 1)[-1]
|
|
859
|
+
label = self.config.taint_source_param_annotations.get(simple)
|
|
860
|
+
if not label:
|
|
861
|
+
continue
|
|
862
|
+
verdict = _annotation_owner_verdict(
|
|
863
|
+
raw_name, at_byte,
|
|
864
|
+
TAINT_SOURCE_PARAM_ANNOTATION_OWNERS.get(simple), self._type_ctx,
|
|
865
|
+
)
|
|
866
|
+
if verdict == "rejected":
|
|
867
|
+
continue
|
|
868
|
+
taint = label
|
|
869
|
+
break
|
|
870
|
+
d = self._new_def(name, _node_pos(params_node), False, taint)
|
|
871
|
+
entry.events.append(d)
|
|
872
|
+
|
|
873
|
+
body = func_node.child_by_field_name("body")
|
|
874
|
+
if body is not None:
|
|
875
|
+
tail = self._process_stmts(_body_stmts(body), entry, exit_b, source)
|
|
876
|
+
if tail is not None:
|
|
877
|
+
self._link(tail, exit_b)
|
|
878
|
+
else:
|
|
879
|
+
self._link(entry, exit_b)
|
|
880
|
+
|
|
881
|
+
return entry.id, exit_b.id, self._blocks, self._all_defs
|
|
882
|
+
|
|
883
|
+
# ── taint helper ──────────────────────────────────────────────────────
|
|
884
|
+
|
|
885
|
+
def _taint_of(self, node: Optional[Node], source: bytes) -> Optional[str]:
|
|
886
|
+
return _source_label_for(node, self.config, source, self._type_ctx)
|
|
887
|
+
|
|
888
|
+
# ── statement dispatch ────────────────────────────────────────────────
|
|
889
|
+
|
|
890
|
+
def _process_stmts(
|
|
891
|
+
self, stmts: List[Node], cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
892
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
893
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
|
|
894
|
+
) -> Optional[CFGBlock]:
|
|
895
|
+
for stmt in stmts:
|
|
896
|
+
if cur is None:
|
|
897
|
+
return None
|
|
898
|
+
cur = self._process_stmt(stmt, cur, exit_b, source, break_target, continue_target, labels)
|
|
899
|
+
return cur
|
|
900
|
+
|
|
901
|
+
def _process_stmt(
|
|
902
|
+
self, stmt: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
903
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
904
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
|
|
905
|
+
) -> Optional[CFGBlock]:
|
|
906
|
+
t = stmt.type
|
|
907
|
+
|
|
908
|
+
if t == "expression_statement":
|
|
909
|
+
real = [c for c in stmt.children if not c.is_extra and c.type != ";"]
|
|
910
|
+
if len(real) == 1:
|
|
911
|
+
return self._process_stmt(real[0], cur, exit_b, source, break_target, continue_target, labels)
|
|
912
|
+
cur.events.extend(_uses_of(stmt))
|
|
913
|
+
return cur
|
|
914
|
+
|
|
915
|
+
if t == "local_variable_declaration":
|
|
916
|
+
for decl in _fields(stmt, "declarator"):
|
|
917
|
+
name_node = decl.child_by_field_name("name")
|
|
918
|
+
value = decl.child_by_field_name("value")
|
|
919
|
+
if name_node is None:
|
|
920
|
+
continue
|
|
921
|
+
is_lit = _is_literal(value)
|
|
922
|
+
taint = self._taint_of(value, source)
|
|
923
|
+
if value is not None:
|
|
924
|
+
cur.events.extend(_uses_of(value))
|
|
925
|
+
cur.events.append(
|
|
926
|
+
self._new_def(_text(name_node, source), _node_pos(name_node), is_lit, taint)
|
|
927
|
+
)
|
|
928
|
+
return cur
|
|
929
|
+
|
|
930
|
+
if t == "assignment_expression":
|
|
931
|
+
lhs = stmt.child_by_field_name("left")
|
|
932
|
+
op = stmt.child_by_field_name("operator")
|
|
933
|
+
rhs = stmt.child_by_field_name("right")
|
|
934
|
+
op_text = _text(op, source) if op is not None else "="
|
|
935
|
+
if rhs is not None:
|
|
936
|
+
cur.events.extend(_uses_of(rhs))
|
|
937
|
+
if lhs is not None and lhs.type == "identifier":
|
|
938
|
+
name = _text(lhs, source)
|
|
939
|
+
if op_text != "=":
|
|
940
|
+
cur.events.append(CFGUse(name, _node_pos(lhs)))
|
|
941
|
+
is_lit = _is_literal(rhs) if op_text == "=" else False
|
|
942
|
+
taint = self._taint_of(rhs, source) if op_text == "=" else None
|
|
943
|
+
cur.events.append(self._new_def(name, _node_pos(lhs), is_lit, taint))
|
|
944
|
+
return cur
|
|
945
|
+
|
|
946
|
+
if t == "update_expression":
|
|
947
|
+
ident = next((c for c in stmt.children if c.type == "identifier"), None)
|
|
948
|
+
if ident is not None:
|
|
949
|
+
name = _text(ident, source)
|
|
950
|
+
cur.events.append(CFGUse(name, _node_pos(ident)))
|
|
951
|
+
cur.events.append(self._new_def(name, _node_pos(ident), False, None))
|
|
952
|
+
return cur
|
|
953
|
+
|
|
954
|
+
if t in ("return_statement", "throw_statement"):
|
|
955
|
+
expr = _first_named_child(stmt)
|
|
956
|
+
if expr is not None:
|
|
957
|
+
cur.events.extend(_uses_of(expr))
|
|
958
|
+
self._link(cur, exit_b)
|
|
959
|
+
return None
|
|
960
|
+
|
|
961
|
+
if t == "break_statement":
|
|
962
|
+
# A labeled `break <label>;` targets that specific loop/switch's
|
|
963
|
+
# own merge block (via `labels`, see module docstring), not
|
|
964
|
+
# necessarily the nearest one. Unlabeled falls back to
|
|
965
|
+
# `break_target`, and that falls back to `exit_b` only for
|
|
966
|
+
# malformed/unparseable code where no enclosing loop/switch was
|
|
967
|
+
# tracked (a bare `break` outside one is not valid Java).
|
|
968
|
+
label_node = next((c for c in stmt.children if c.type == "identifier"), None)
|
|
969
|
+
target = break_target
|
|
970
|
+
if label_node is not None and labels:
|
|
971
|
+
named = labels.get(_text(label_node, source))
|
|
972
|
+
if named is not None:
|
|
973
|
+
target = named[0]
|
|
974
|
+
self._link(cur, target if target is not None else exit_b)
|
|
975
|
+
return None
|
|
976
|
+
|
|
977
|
+
if t == "continue_statement":
|
|
978
|
+
label_node = next((c for c in stmt.children if c.type == "identifier"), None)
|
|
979
|
+
target = continue_target
|
|
980
|
+
if label_node is not None and labels:
|
|
981
|
+
named = labels.get(_text(label_node, source))
|
|
982
|
+
if named is not None:
|
|
983
|
+
target = named[1]
|
|
984
|
+
self._link(cur, target if target is not None else exit_b)
|
|
985
|
+
return None
|
|
986
|
+
|
|
987
|
+
if t == "assert_statement":
|
|
988
|
+
for child in stmt.children:
|
|
989
|
+
if child.is_named:
|
|
990
|
+
cur.events.extend(_uses_of(child))
|
|
991
|
+
return cur
|
|
992
|
+
|
|
993
|
+
if t in _SIMPLE_STMT_TYPES:
|
|
994
|
+
cur.events.extend(_uses_of(stmt))
|
|
995
|
+
return cur
|
|
996
|
+
|
|
997
|
+
if t == "if_statement":
|
|
998
|
+
return self._process_if(stmt, cur, exit_b, source, break_target, continue_target, labels)
|
|
999
|
+
|
|
1000
|
+
if t == "for_statement":
|
|
1001
|
+
return self._process_for(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
|
|
1002
|
+
|
|
1003
|
+
if t == "enhanced_for_statement":
|
|
1004
|
+
return self._process_enhanced_for(
|
|
1005
|
+
stmt, cur, exit_b, source, break_target, continue_target, labels, label
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
if t == "while_statement":
|
|
1009
|
+
return self._process_while(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
|
|
1010
|
+
|
|
1011
|
+
if t == "do_statement":
|
|
1012
|
+
return self._process_do_while(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
|
|
1013
|
+
|
|
1014
|
+
if t in ("switch_expression", "switch_statement"):
|
|
1015
|
+
return self._process_switch(stmt, cur, exit_b, source, break_target, continue_target, labels, label)
|
|
1016
|
+
|
|
1017
|
+
if t in ("try_statement", "try_with_resources_statement"):
|
|
1018
|
+
return self._process_try(stmt, cur, exit_b, source, break_target, continue_target, labels)
|
|
1019
|
+
|
|
1020
|
+
if t == "synchronized_statement":
|
|
1021
|
+
return self._process_synchronized(stmt, cur, exit_b, source, break_target, continue_target, labels)
|
|
1022
|
+
|
|
1023
|
+
if t == "labeled_statement":
|
|
1024
|
+
# Only threaded into the five loop/switch handlers above (via
|
|
1025
|
+
# their own `label` parameter) — a label on any other statement
|
|
1026
|
+
# kind is dropped; see module docstring.
|
|
1027
|
+
label_node = next((c for c in stmt.children if c.type == "identifier"), None)
|
|
1028
|
+
inner = next((c for c in reversed(stmt.children) if c.is_named), None)
|
|
1029
|
+
if inner is not None:
|
|
1030
|
+
label_text = _text(label_node, source) if label_node is not None else None
|
|
1031
|
+
return self._process_stmt(
|
|
1032
|
+
inner, cur, exit_b, source, break_target, continue_target, labels, label_text
|
|
1033
|
+
)
|
|
1034
|
+
return cur
|
|
1035
|
+
|
|
1036
|
+
if t == "block":
|
|
1037
|
+
return self._process_stmts(_real_children(stmt), cur, exit_b, source, break_target, continue_target, labels)
|
|
1038
|
+
|
|
1039
|
+
if t in _TYPE_DECL_TYPES:
|
|
1040
|
+
# Local/nested type declaration — opaque, no defs/uses of its own.
|
|
1041
|
+
return cur
|
|
1042
|
+
|
|
1043
|
+
# ── fallback: conservative — collect all identifiers as uses ──────
|
|
1044
|
+
cur.events.extend(_uses_of(stmt))
|
|
1045
|
+
return cur
|
|
1046
|
+
|
|
1047
|
+
# ── compound statement helpers ────────────────────────────────────────
|
|
1048
|
+
|
|
1049
|
+
def _process_if(
|
|
1050
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1051
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1052
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
|
|
1053
|
+
) -> Optional[CFGBlock]:
|
|
1054
|
+
cond = node.child_by_field_name("condition")
|
|
1055
|
+
if cond is not None:
|
|
1056
|
+
cur.events.extend(_uses_of(cond))
|
|
1057
|
+
|
|
1058
|
+
merge = self._new_block()
|
|
1059
|
+
|
|
1060
|
+
then_body = node.child_by_field_name("consequence")
|
|
1061
|
+
if then_body is not None:
|
|
1062
|
+
then_b = self._new_block()
|
|
1063
|
+
self._link(cur, then_b)
|
|
1064
|
+
tail = self._process_stmts(
|
|
1065
|
+
_body_stmts(then_body), then_b, exit_b, source, break_target, continue_target, labels
|
|
1066
|
+
)
|
|
1067
|
+
if tail is not None:
|
|
1068
|
+
self._link(tail, merge)
|
|
1069
|
+
else:
|
|
1070
|
+
self._link(cur, merge)
|
|
1071
|
+
|
|
1072
|
+
alt = node.child_by_field_name("alternative")
|
|
1073
|
+
if alt is None:
|
|
1074
|
+
self._link(cur, merge)
|
|
1075
|
+
elif alt.type == "if_statement":
|
|
1076
|
+
alt_b = self._new_block()
|
|
1077
|
+
self._link(cur, alt_b)
|
|
1078
|
+
tail = self._process_stmt(alt, alt_b, exit_b, source, break_target, continue_target, labels)
|
|
1079
|
+
if tail is not None:
|
|
1080
|
+
self._link(tail, merge)
|
|
1081
|
+
else:
|
|
1082
|
+
alt_b = self._new_block()
|
|
1083
|
+
self._link(cur, alt_b)
|
|
1084
|
+
tail = self._process_stmts(
|
|
1085
|
+
_body_stmts(alt), alt_b, exit_b, source, break_target, continue_target, labels
|
|
1086
|
+
)
|
|
1087
|
+
if tail is not None:
|
|
1088
|
+
self._link(tail, merge)
|
|
1089
|
+
|
|
1090
|
+
return merge
|
|
1091
|
+
|
|
1092
|
+
def _process_for(
|
|
1093
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1094
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1095
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
|
|
1096
|
+
) -> Optional[CFGBlock]:
|
|
1097
|
+
# init runs once, unconditionally, before the header — not part of
|
|
1098
|
+
# the loop body, so it keeps whatever break/continue target this
|
|
1099
|
+
# `for` statement itself is nested in (irrelevant in valid Java: an
|
|
1100
|
+
# `init` clause can't contain a bare `break`/`continue`).
|
|
1101
|
+
for init in _fields(node, "init"):
|
|
1102
|
+
cur = self._process_stmt(init, cur, exit_b, source, break_target, continue_target, labels)
|
|
1103
|
+
if cur is None:
|
|
1104
|
+
return None
|
|
1105
|
+
|
|
1106
|
+
header = self._new_block()
|
|
1107
|
+
body_b = self._new_block()
|
|
1108
|
+
# `continue`'s target is a dedicated block preceding the `update`
|
|
1109
|
+
# clause (not the body's own tail block) — Java's `for`-loop
|
|
1110
|
+
# `continue` runs `update` before the condition is re-checked, and
|
|
1111
|
+
# a `continue` may fire before the body's tail is known (e.g. from
|
|
1112
|
+
# inside a nested `if`).
|
|
1113
|
+
update_b = self._new_block()
|
|
1114
|
+
after = self._new_block()
|
|
1115
|
+
self._link(cur, header)
|
|
1116
|
+
|
|
1117
|
+
cond = node.child_by_field_name("condition")
|
|
1118
|
+
if cond is not None:
|
|
1119
|
+
header.events.extend(_uses_of(cond))
|
|
1120
|
+
|
|
1121
|
+
self._link(header, body_b)
|
|
1122
|
+
self._link(header, after)
|
|
1123
|
+
|
|
1124
|
+
# If this `for` is itself directly labeled (`outer: for (...) {}`),
|
|
1125
|
+
# `break outer;`/`continue outer;` anywhere inside its body —
|
|
1126
|
+
# including past a nested loop — must resolve to *this* loop's own
|
|
1127
|
+
# targets, not whatever loop most closely encloses them.
|
|
1128
|
+
body_labels = {**(labels or {}), label: (after, update_b)} if label else (labels or {})
|
|
1129
|
+
|
|
1130
|
+
body = node.child_by_field_name("body")
|
|
1131
|
+
tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, update_b, body_labels)
|
|
1132
|
+
if tail is not None:
|
|
1133
|
+
self._link(tail, update_b)
|
|
1134
|
+
|
|
1135
|
+
upd_tail = update_b
|
|
1136
|
+
for upd in _fields(node, "update"):
|
|
1137
|
+
upd_tail = self._process_stmt(upd, upd_tail, exit_b, source, break_target, continue_target, labels)
|
|
1138
|
+
if upd_tail is None:
|
|
1139
|
+
break
|
|
1140
|
+
if upd_tail is not None:
|
|
1141
|
+
self._link(upd_tail, header) # back-edge
|
|
1142
|
+
|
|
1143
|
+
return after
|
|
1144
|
+
|
|
1145
|
+
def _process_enhanced_for(
|
|
1146
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1147
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1148
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
|
|
1149
|
+
) -> Optional[CFGBlock]:
|
|
1150
|
+
header = self._new_block()
|
|
1151
|
+
body_b = self._new_block()
|
|
1152
|
+
after = self._new_block()
|
|
1153
|
+
self._link(cur, header)
|
|
1154
|
+
|
|
1155
|
+
value = node.child_by_field_name("value")
|
|
1156
|
+
if value is not None:
|
|
1157
|
+
header.events.extend(_uses_of(value))
|
|
1158
|
+
|
|
1159
|
+
name_node = node.child_by_field_name("name")
|
|
1160
|
+
if name_node is not None:
|
|
1161
|
+
taint = self._taint_of(value, source)
|
|
1162
|
+
header.events.append(
|
|
1163
|
+
self._new_def(_text(name_node, source), _node_pos(name_node), False, taint)
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
self._link(header, body_b)
|
|
1167
|
+
self._link(header, after)
|
|
1168
|
+
|
|
1169
|
+
# `continue` re-fetches the next element — there's no `update`
|
|
1170
|
+
# clause, so it targets the header directly.
|
|
1171
|
+
body_labels = {**(labels or {}), label: (after, header)} if label else (labels or {})
|
|
1172
|
+
|
|
1173
|
+
body = node.child_by_field_name("body")
|
|
1174
|
+
tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, header, body_labels)
|
|
1175
|
+
if tail is not None:
|
|
1176
|
+
self._link(tail, header) # back-edge
|
|
1177
|
+
|
|
1178
|
+
return after
|
|
1179
|
+
|
|
1180
|
+
def _process_while(
|
|
1181
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1182
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1183
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
|
|
1184
|
+
) -> Optional[CFGBlock]:
|
|
1185
|
+
header = self._new_block()
|
|
1186
|
+
body_b = self._new_block()
|
|
1187
|
+
after = self._new_block()
|
|
1188
|
+
self._link(cur, header)
|
|
1189
|
+
|
|
1190
|
+
cond = node.child_by_field_name("condition")
|
|
1191
|
+
if cond is not None:
|
|
1192
|
+
header.events.extend(_uses_of(cond))
|
|
1193
|
+
|
|
1194
|
+
self._link(header, body_b)
|
|
1195
|
+
self._link(header, after)
|
|
1196
|
+
|
|
1197
|
+
# `continue` jumps straight to the condition re-check.
|
|
1198
|
+
body_labels = {**(labels or {}), label: (after, header)} if label else (labels or {})
|
|
1199
|
+
|
|
1200
|
+
body = node.child_by_field_name("body")
|
|
1201
|
+
tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, header, body_labels)
|
|
1202
|
+
if tail is not None:
|
|
1203
|
+
self._link(tail, header) # back-edge
|
|
1204
|
+
|
|
1205
|
+
return after
|
|
1206
|
+
|
|
1207
|
+
def _process_do_while(
|
|
1208
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1209
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1210
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
|
|
1211
|
+
) -> Optional[CFGBlock]:
|
|
1212
|
+
body_b = self._new_block()
|
|
1213
|
+
header = self._new_block()
|
|
1214
|
+
after = self._new_block()
|
|
1215
|
+
self._link(cur, body_b) # body always runs at least once
|
|
1216
|
+
|
|
1217
|
+
# `continue` in a `do`-loop still goes to the condition check
|
|
1218
|
+
# (`header`), which here comes *after* the body syntactically.
|
|
1219
|
+
body_labels = {**(labels or {}), label: (after, header)} if label else (labels or {})
|
|
1220
|
+
|
|
1221
|
+
body = node.child_by_field_name("body")
|
|
1222
|
+
tail = self._process_stmts(_body_stmts(body), body_b, exit_b, source, after, header, body_labels)
|
|
1223
|
+
if tail is not None:
|
|
1224
|
+
self._link(tail, header)
|
|
1225
|
+
|
|
1226
|
+
cond = node.child_by_field_name("condition")
|
|
1227
|
+
if cond is not None:
|
|
1228
|
+
header.events.extend(_uses_of(cond))
|
|
1229
|
+
|
|
1230
|
+
self._link(header, body_b) # back-edge
|
|
1231
|
+
self._link(header, after)
|
|
1232
|
+
|
|
1233
|
+
return after
|
|
1234
|
+
|
|
1235
|
+
def _process_switch(
|
|
1236
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1237
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1238
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None, label: Optional[str] = None,
|
|
1239
|
+
) -> Optional[CFGBlock]:
|
|
1240
|
+
cond = node.child_by_field_name("condition")
|
|
1241
|
+
if cond is not None:
|
|
1242
|
+
cur.events.extend(_uses_of(cond))
|
|
1243
|
+
|
|
1244
|
+
merge = self._new_block()
|
|
1245
|
+
switch_body = node.child_by_field_name("body")
|
|
1246
|
+
groups: List[Node] = []
|
|
1247
|
+
if switch_body is not None:
|
|
1248
|
+
groups = [c for c in switch_body.children
|
|
1249
|
+
if c.type in ("switch_block_statement_group", "switch_rule")]
|
|
1250
|
+
|
|
1251
|
+
# Conservative fallback: no case (including `default`, if present) is
|
|
1252
|
+
# guaranteed to execute from a static analysis's point of view, so a
|
|
1253
|
+
# pre-switch def must still be considered reaching the merge block —
|
|
1254
|
+
# same "extra reachable pair is safe, a missed one is not" philosophy
|
|
1255
|
+
# `PythonCFGBuilder._process_if`'s no-else fallthrough edge follows.
|
|
1256
|
+
self._link(cur, merge)
|
|
1257
|
+
|
|
1258
|
+
# `break` inside a case targets this switch's own merge block (the
|
|
1259
|
+
# statement after the switch) — a plain, unlabeled `continue`,
|
|
1260
|
+
# though, isn't caught by the switch at all; it still refers to
|
|
1261
|
+
# whatever loop lexically encloses the switch, so `continue_target`
|
|
1262
|
+
# passes through unchanged. If this switch is itself labeled
|
|
1263
|
+
# (`outer: switch (...) {}`), `break outer;`/`continue outer;` from
|
|
1264
|
+
# deeper inside must resolve to *this* switch's targets specifically.
|
|
1265
|
+
body_labels = {**(labels or {}), label: (merge, continue_target)} if label else (labels or {})
|
|
1266
|
+
|
|
1267
|
+
for group in groups:
|
|
1268
|
+
group_b = self._new_block()
|
|
1269
|
+
self._link(cur, group_b) # conservative: every case reachable
|
|
1270
|
+
stmts = [c for c in group.children
|
|
1271
|
+
if c.is_named and c.type != "switch_label"]
|
|
1272
|
+
tail = self._process_stmts(stmts, group_b, exit_b, source, merge, continue_target, body_labels)
|
|
1273
|
+
if tail is not None:
|
|
1274
|
+
self._link(tail, merge)
|
|
1275
|
+
|
|
1276
|
+
return merge
|
|
1277
|
+
|
|
1278
|
+
def _process_try(
|
|
1279
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1280
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1281
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
|
|
1282
|
+
) -> Optional[CFGBlock]:
|
|
1283
|
+
merge = self._new_block()
|
|
1284
|
+
|
|
1285
|
+
finally_clause = next((c for c in node.children if c.type == "finally_clause"), None)
|
|
1286
|
+
fin_body = None
|
|
1287
|
+
if finally_clause is not None:
|
|
1288
|
+
fin_body = next((c for c in finally_clause.children if c.type == "block"), None)
|
|
1289
|
+
|
|
1290
|
+
# An active `finally` intercepts every abrupt completion (`return`,
|
|
1291
|
+
# `throw`, `break`, `continue`) from the try body and catch handlers
|
|
1292
|
+
# before it actually reaches whichever target that completion was
|
|
1293
|
+
# headed for (function exit, an enclosing loop/switch's break point,
|
|
1294
|
+
# an enclosing loop's continue point). Each distinct target that's
|
|
1295
|
+
# actually active here gets its own copy of the finally body — the
|
|
1296
|
+
# continuations differ (chasing `exit_b` vs. `break_target` vs.
|
|
1297
|
+
# `continue_target`), and this also chains correctly through
|
|
1298
|
+
# arbitrarily nested `try`/`finally`, since a target this method
|
|
1299
|
+
# itself received may already be *another* enclosing try's own
|
|
1300
|
+
# relay block. A `return`/`break`/`continue` written directly inside
|
|
1301
|
+
# the finally body itself is unaffected — it still resolves against
|
|
1302
|
+
# this try's own `exit_b`/`break_target`/`continue_target`, per
|
|
1303
|
+
# normal lexical scoping, not the relayed one.
|
|
1304
|
+
def _relay(target: Optional[CFGBlock]) -> Optional[CFGBlock]:
|
|
1305
|
+
if target is None or fin_body is None:
|
|
1306
|
+
return target
|
|
1307
|
+
relay_b = self._new_block()
|
|
1308
|
+
tail = self._process_stmts(
|
|
1309
|
+
_real_children(fin_body), relay_b, exit_b, source, break_target, continue_target, labels
|
|
1310
|
+
)
|
|
1311
|
+
if tail is not None:
|
|
1312
|
+
self._link(tail, target)
|
|
1313
|
+
return relay_b
|
|
1314
|
+
|
|
1315
|
+
abrupt_exit = _relay(exit_b)
|
|
1316
|
+
abrupt_break = _relay(break_target)
|
|
1317
|
+
abrupt_continue = _relay(continue_target)
|
|
1318
|
+
# A labeled break/continue escaping this try must run `finally`
|
|
1319
|
+
# too, same as the unlabeled/exit cases above — relay every active
|
|
1320
|
+
# label's own (break, continue) target pair the same way.
|
|
1321
|
+
abrupt_labels = (
|
|
1322
|
+
{name: (_relay(brk), _relay(cont)) for name, (brk, cont) in labels.items()}
|
|
1323
|
+
if labels else labels
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
pre = cur
|
|
1327
|
+
resources = node.child_by_field_name("resources")
|
|
1328
|
+
if resources is not None:
|
|
1329
|
+
res_b = self._new_block()
|
|
1330
|
+
self._link(cur, res_b)
|
|
1331
|
+
for res in resources.children:
|
|
1332
|
+
if res.type != "resource":
|
|
1333
|
+
continue
|
|
1334
|
+
name_node = res.child_by_field_name("name")
|
|
1335
|
+
value = res.child_by_field_name("value")
|
|
1336
|
+
if value is not None:
|
|
1337
|
+
res_b.events.extend(_uses_of(value))
|
|
1338
|
+
if name_node is not None:
|
|
1339
|
+
taint = self._taint_of(value, source)
|
|
1340
|
+
res_b.events.append(
|
|
1341
|
+
self._new_def(_text(name_node, source), _node_pos(name_node), False, taint)
|
|
1342
|
+
)
|
|
1343
|
+
pre = res_b
|
|
1344
|
+
|
|
1345
|
+
body = node.child_by_field_name("body")
|
|
1346
|
+
if body is not None:
|
|
1347
|
+
try_b = self._new_block()
|
|
1348
|
+
self._link(pre, try_b)
|
|
1349
|
+
tail = self._process_stmts(
|
|
1350
|
+
_real_children(body), try_b, abrupt_exit, source, abrupt_break, abrupt_continue, abrupt_labels
|
|
1351
|
+
)
|
|
1352
|
+
if tail is not None:
|
|
1353
|
+
self._link(tail, merge)
|
|
1354
|
+
else:
|
|
1355
|
+
self._link(pre, merge)
|
|
1356
|
+
|
|
1357
|
+
for child in node.children:
|
|
1358
|
+
if child.type != "catch_clause":
|
|
1359
|
+
continue
|
|
1360
|
+
exc_b = self._new_block()
|
|
1361
|
+
self._link(pre, exc_b) # conservative: reachable from try entry
|
|
1362
|
+
param = next(
|
|
1363
|
+
(c for c in child.children if c.type == "catch_formal_parameter"), None
|
|
1364
|
+
)
|
|
1365
|
+
if param is not None:
|
|
1366
|
+
name_node = param.child_by_field_name("name")
|
|
1367
|
+
if name_node is not None:
|
|
1368
|
+
exc_b.events.append(
|
|
1369
|
+
self._new_def(_text(name_node, source), _node_pos(name_node), False, None)
|
|
1370
|
+
)
|
|
1371
|
+
exc_body = child.child_by_field_name("body")
|
|
1372
|
+
if exc_body is not None:
|
|
1373
|
+
tail = self._process_stmts(
|
|
1374
|
+
_real_children(exc_body), exc_b, abrupt_exit, source, abrupt_break, abrupt_continue,
|
|
1375
|
+
abrupt_labels,
|
|
1376
|
+
)
|
|
1377
|
+
if tail is not None:
|
|
1378
|
+
self._link(tail, merge)
|
|
1379
|
+
else:
|
|
1380
|
+
self._link(exc_b, merge)
|
|
1381
|
+
|
|
1382
|
+
if fin_body is not None:
|
|
1383
|
+
fin_b = self._new_block()
|
|
1384
|
+
self._link(merge, fin_b)
|
|
1385
|
+
tail = self._process_stmts(
|
|
1386
|
+
_real_children(fin_body), fin_b, exit_b, source, break_target, continue_target, labels
|
|
1387
|
+
)
|
|
1388
|
+
if tail is not None:
|
|
1389
|
+
merge = tail
|
|
1390
|
+
|
|
1391
|
+
return merge
|
|
1392
|
+
|
|
1393
|
+
def _process_synchronized(
|
|
1394
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes,
|
|
1395
|
+
break_target: Optional[CFGBlock] = None, continue_target: Optional[CFGBlock] = None,
|
|
1396
|
+
labels: Optional[Dict[str, Tuple[CFGBlock, CFGBlock]]] = None,
|
|
1397
|
+
) -> Optional[CFGBlock]:
|
|
1398
|
+
lock = next((c for c in node.children if c.type == "parenthesized_expression"), None)
|
|
1399
|
+
if lock is not None:
|
|
1400
|
+
cur.events.extend(_uses_of(lock))
|
|
1401
|
+
body = node.child_by_field_name("body")
|
|
1402
|
+
return self._process_stmts(_body_stmts(body), cur, exit_b, source, break_target, continue_target, labels)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def default_java_cfg_config() -> CFGConfig:
|
|
1406
|
+
"""The real Java `CFGConfig` — returned by `JavaBackend.cfg_config()`."""
|
|
1407
|
+
from adl_drift.lang.java.taint import default_java_taint_strategy
|
|
1408
|
+
|
|
1409
|
+
return CFGConfig(
|
|
1410
|
+
taint_sources=TAINT_SOURCES,
|
|
1411
|
+
taint_source_methods=dict(TAINT_SOURCE_METHODS),
|
|
1412
|
+
taint_source_param_annotations=dict(TAINT_SOURCE_PARAM_ANNOTATIONS),
|
|
1413
|
+
sink_methods=dict(SINK_METHODS),
|
|
1414
|
+
sink_constructors=dict(SINK_CONSTRUCTORS),
|
|
1415
|
+
cfg_builder_factory=lambda config: JavaCFGBuilder(config),
|
|
1416
|
+
taint_strategy_factory=lambda config: default_java_taint_strategy(config),
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def analyze_function(func_node: Node, source: bytes, function_id: str):
|
|
1421
|
+
"""Convenience wrapper mirroring `graph.cfg.analyze_function`, pinned to
|
|
1422
|
+
Java's `CFGConfig` — used directly by Java CFG unit tests."""
|
|
1423
|
+
from adl_drift.graph.cfg import analyze_function as _analyze_function
|
|
1424
|
+
return _analyze_function(func_node, source, function_id, default_java_cfg_config())
|