coretrace-python-analyzer 0.1.0__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.
Files changed (126) hide show
  1. coretrace_python/__init__.py +4 -0
  2. coretrace_python/__main__.py +4 -0
  3. coretrace_python/abstract/__init__.py +45 -0
  4. coretrace_python/abstract/constants.py +226 -0
  5. coretrace_python/abstract/heap.py +252 -0
  6. coretrace_python/abstract/ranges.py +285 -0
  7. coretrace_python/abstract/values.py +56 -0
  8. coretrace_python/analysis/__init__.py +31 -0
  9. coretrace_python/analysis/manager.py +165 -0
  10. coretrace_python/analysis/provider.py +73 -0
  11. coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
  12. coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
  13. coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
  14. coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
  15. coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
  16. coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
  17. coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
  18. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
  19. coretrace_python/bundled/models/cli/cli_models.py +35 -0
  20. coretrace_python/bundled/models/cli/plugin.toml +9 -0
  21. coretrace_python/bundled/models/credentials/credential_models.py +43 -0
  22. coretrace_python/bundled/models/credentials/plugin.toml +9 -0
  23. coretrace_python/bundled/models/django/django_models.py +123 -0
  24. coretrace_python/bundled/models/django/plugin.toml +9 -0
  25. coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
  26. coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
  27. coretrace_python/bundled/models/flask/flask_models.py +53 -0
  28. coretrace_python/bundled/models/flask/plugin.toml +9 -0
  29. coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
  30. coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
  31. coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
  32. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
  33. coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
  34. coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
  35. coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
  36. coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
  37. coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
  38. coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
  39. coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
  40. coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
  41. coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
  42. coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
  43. coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
  44. coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
  45. coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
  46. coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
  47. coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
  48. coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
  49. coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
  50. coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
  51. coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
  52. coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
  53. coretrace_python/bundled/security/xss/plugin.toml +9 -0
  54. coretrace_python/bundled/security/xss/xss.py +17 -0
  55. coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
  56. coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
  57. coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
  58. coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
  59. coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
  60. coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
  61. coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
  62. coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
  63. coretrace_python/cache.py +310 -0
  64. coretrace_python/cfg/__init__.py +46 -0
  65. coretrace_python/cfg/builder.py +589 -0
  66. coretrace_python/cfg/dominance.py +183 -0
  67. coretrace_python/cfg/model.py +166 -0
  68. coretrace_python/cli.py +216 -0
  69. coretrace_python/dataflow/__init__.py +29 -0
  70. coretrace_python/dataflow/lattice.py +78 -0
  71. coretrace_python/dataflow/solver.py +96 -0
  72. coretrace_python/dependency/__init__.py +44 -0
  73. coretrace_python/dependency/advisories.py +168 -0
  74. coretrace_python/dependency/correlation.py +87 -0
  75. coretrace_python/dependency/graph.py +274 -0
  76. coretrace_python/dependency/policy.py +65 -0
  77. coretrace_python/dependency/sbom.py +65 -0
  78. coretrace_python/engine.py +688 -0
  79. coretrace_python/findings/__init__.py +13 -0
  80. coretrace_python/findings/coverage.py +45 -0
  81. coretrace_python/findings/model.py +49 -0
  82. coretrace_python/findings/refutation.py +427 -0
  83. coretrace_python/frontend/__init__.py +18 -0
  84. coretrace_python/frontend/ast_adapter.py +447 -0
  85. coretrace_python/frontend/parser.py +23 -0
  86. coretrace_python/hir/__init__.py +5 -0
  87. coretrace_python/hir/nodes.py +584 -0
  88. coretrace_python/hir/visitors.py +36 -0
  89. coretrace_python/interprocedural/__init__.py +49 -0
  90. coretrace_python/interprocedural/callgraph.py +291 -0
  91. coretrace_python/interprocedural/modulegraph.py +218 -0
  92. coretrace_python/interprocedural/summaries.py +463 -0
  93. coretrace_python/ir/__init__.py +5 -0
  94. coretrace_python/ir/defuse.py +74 -0
  95. coretrace_python/ir/lowering.py +575 -0
  96. coretrace_python/ir/model.py +481 -0
  97. coretrace_python/ir/printer.py +201 -0
  98. coretrace_python/ir/ssa.py +277 -0
  99. coretrace_python/plugins/__init__.py +58 -0
  100. coretrace_python/plugins/api.py +153 -0
  101. coretrace_python/plugins/detectors.py +114 -0
  102. coretrace_python/plugins/loader.py +84 -0
  103. coretrace_python/plugins/manifest.py +112 -0
  104. coretrace_python/plugins/registry.py +34 -0
  105. coretrace_python/plugins/secrets.py +330 -0
  106. coretrace_python/reporters/__init__.py +22 -0
  107. coretrace_python/reporters/json_format.py +48 -0
  108. coretrace_python/reporters/report.py +23 -0
  109. coretrace_python/reporters/sarif.py +70 -0
  110. coretrace_python/reporters/text.py +24 -0
  111. coretrace_python/semantic/__init__.py +9 -0
  112. coretrace_python/semantic/identity.py +39 -0
  113. coretrace_python/semantic/imports.py +131 -0
  114. coretrace_python/semantic/scopes.py +473 -0
  115. coretrace_python/semantic/symbols.py +87 -0
  116. coretrace_python/source/__init__.py +13 -0
  117. coretrace_python/source/manager.py +81 -0
  118. coretrace_python/source/model.py +57 -0
  119. coretrace_python/taint/__init__.py +55 -0
  120. coretrace_python/taint/engine.py +800 -0
  121. coretrace_python/taint/models.py +317 -0
  122. coretrace_python/taint/routes.py +99 -0
  123. coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
  124. coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
  125. coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
  126. coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,800 @@
1
+ """The global taint engine (architecture §17).
2
+
3
+ One forward data-flow problem over the SSA form of a function. Values defined by a
4
+ source symbol carry the source's taint kinds; arithmetic, attribute and item access,
5
+ iteration, phis and calls propagate the union of their operands' taint; sanitizer
6
+ calls clear their kinds; comparisons and literals carry nothing. Every tainted
7
+ argument reaching a sink whose kinds it still carries is reported as a ``TaintFlow``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable, Mapping
13
+ from dataclasses import dataclass
14
+ from types import MappingProxyType
15
+ from typing import ClassVar
16
+
17
+ from coretrace_python.abstract import (
18
+ ATTRIBUTES,
19
+ ELEMENTS,
20
+ HeapAnalysis,
21
+ HeapFacts,
22
+ HeapLocation,
23
+ mutated_by,
24
+ )
25
+ from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
26
+ from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
27
+ from coretrace_python.dataflow import DataflowProblem, Direction, solve
28
+ from coretrace_python.hir import nodes
29
+ from coretrace_python.interprocedural import (
30
+ CallGraph,
31
+ CallGraphAnalysis,
32
+ ExternalSymbol,
33
+ FunctionSummary,
34
+ KnownFunction,
35
+ ProjectSummaries,
36
+ SummaryAnalysis,
37
+ SummaryIndex,
38
+ SummaryTable,
39
+ project_symbol,
40
+ )
41
+ from coretrace_python.ir.lowering import analyzable_functions
42
+ from coretrace_python.ir.model import (
43
+ BasicBlock,
44
+ Branch,
45
+ BuildDict,
46
+ BuildList,
47
+ BuildSet,
48
+ BuildTuple,
49
+ Call,
50
+ Compare,
51
+ ForNext,
52
+ FunctionIR,
53
+ GetAttr,
54
+ GetItem,
55
+ GetIter,
56
+ Global,
57
+ Instruction,
58
+ Jump,
59
+ MakeFunction,
60
+ Phi,
61
+ SetAttr,
62
+ SetItem,
63
+ Symbol,
64
+ Value,
65
+ )
66
+ from coretrace_python.ir.ssa import SSAAnalysis
67
+ from coretrace_python.semantic.scopes import ScopeAnalysis, ScopeTable
68
+ from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId, SymbolTable
69
+ from coretrace_python.source import SourceSpan
70
+ from coretrace_python.taint.models import (
71
+ EntryPoint,
72
+ ModelTable,
73
+ SecurityModelAnalysis,
74
+ Sink,
75
+ Source,
76
+ TaintKind,
77
+ )
78
+ from coretrace_python.taint.routes import RegisteredRoutes, Routes
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class Taint:
83
+ kinds: TaintKind
84
+ sources: frozenset[Source]
85
+
86
+ @classmethod
87
+ def none(cls) -> Taint:
88
+ return cls(TaintKind.NONE, frozenset())
89
+
90
+ def __bool__(self) -> bool:
91
+ return self.kinds is not TaintKind.NONE and bool(self.kinds)
92
+
93
+ def join(self, other: Taint) -> Taint:
94
+ return Taint(self.kinds | other.kinds, self.sources | other.sources)
95
+
96
+ def without(self, kinds: TaintKind) -> Taint:
97
+ remaining = self.kinds & ~kinds
98
+ return Taint(remaining, self.sources if remaining else frozenset())
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class TaintFlow:
103
+ """``location`` is the call in the analysed function; when the sink is reached
104
+ inside a known callee, ``through`` names it and ``sink_location`` points at the sink."""
105
+
106
+ source: Source
107
+ sink: Sink
108
+ kinds: TaintKind
109
+ argument: Value
110
+ location: SourceSpan
111
+ through: str | None = None
112
+ sink_location: SourceSpan | None = None
113
+
114
+
115
+ Key = Value | HeapLocation
116
+
117
+
118
+ class TaintFacts:
119
+ def __init__(self, taints: Mapping[Key, Taint], flows: tuple[TaintFlow, ...]) -> None:
120
+ self._taints = MappingProxyType(dict(taints))
121
+ self.flows = flows
122
+
123
+ def taint(self, value: Value) -> Taint:
124
+ return self._taints.get(value, Taint.none())
125
+
126
+ def heap(self, location: HeapLocation) -> Taint:
127
+ """The taint stored in one field of one abstract object (§22)."""
128
+
129
+ return self._taints.get(location, Taint.none())
130
+
131
+
132
+ State = Mapping[Key, Taint]
133
+
134
+
135
+ class _TaintProblem(DataflowProblem[State]):
136
+ direction: ClassVar[Direction] = Direction.FORWARD
137
+
138
+ def __init__(
139
+ self,
140
+ name: str,
141
+ function: FunctionIR,
142
+ models: ModelTable,
143
+ graph: CallGraph,
144
+ summaries: SummaryTable,
145
+ parameters: Mapping[int, Source] | None = None,
146
+ project: SummaryIndex | None = None,
147
+ heap: HeapFacts | None = None,
148
+ seeds: Mapping[HeapLocation, Taint] | None = None,
149
+ ) -> None:
150
+ self.name = name
151
+ self.function = function
152
+ self.models = models
153
+ self.graph = graph
154
+ self.summaries = summaries
155
+ self.parameters = parameters or {}
156
+ self.project = project or SummaryIndex()
157
+ self.heap = heap or HeapFacts({})
158
+ self.seeds = dict(seeds or {})
159
+ self.blocks = {block.id: block for block in function.blocks}
160
+ self.defs: dict[Value, Instruction] = {
161
+ i.result: i for block in function.blocks for i in block.instructions if i.result
162
+ }
163
+ self.symbols = graph.symbols(name)
164
+
165
+ # ------------------------------------------------------------------ heap
166
+
167
+ def deep(self, value: Value, state: Mapping[Key, Taint]) -> Taint:
168
+ """The taint of a value and of the contents of the objects it points to."""
169
+
170
+ taint = state.get(value, Taint.none())
171
+ for field in (ELEMENTS, ATTRIBUTES):
172
+ for location in self.heap.locations(value, field):
173
+ taint = taint.join(state.get(location, Taint.none()))
174
+ return taint
175
+
176
+ def store(self, state: dict[Key, Taint], receiver: Value, field: str, taint: Taint) -> None:
177
+ for location in self.heap.locations(receiver, field):
178
+ state[location] = state.get(location, Taint.none()).join(taint)
179
+
180
+ def loaded(self, instruction: GetAttr | GetItem | GetIter, state: Mapping[Key, Taint]) -> Taint:
181
+ if isinstance(instruction, GetAttr):
182
+ receiver, field = instruction.object, ATTRIBUTES
183
+ elif isinstance(instruction, GetItem):
184
+ receiver, field = instruction.object, ELEMENTS
185
+ else:
186
+ receiver, field = instruction.iterable, ELEMENTS
187
+ taint = Taint.none()
188
+ for location in self.heap.locations(receiver, field):
189
+ taint = taint.join(state.get(location, Taint.none()))
190
+ return taint
191
+
192
+ def initial(self) -> State:
193
+ state: dict[Key, Taint] = {
194
+ self.function.parameters[index]: Taint(source.kinds, frozenset({source}))
195
+ for index, source in self.parameters.items()
196
+ if index < len(self.function.parameters)
197
+ }
198
+ for location, taint in self.seeds.items():
199
+ state[location] = taint
200
+ return MappingProxyType(state)
201
+
202
+ def join(self, a: State, b: State) -> State:
203
+ merged = dict(a)
204
+ for value, taint in b.items():
205
+ merged[value] = merged[value].join(taint) if value in merged else taint
206
+ return MappingProxyType(merged)
207
+
208
+ def evaluate(
209
+ self, block: BasicBlock, incoming: Mapping[BlockId, State]
210
+ ) -> tuple[State, list[TaintFlow]]:
211
+ states = list(incoming.values())
212
+ state: dict[Key, Taint] = dict(states[0]) if states else {}
213
+ for other in states[1:]:
214
+ state = dict(self.join(state, other))
215
+ flows: list[TaintFlow] = []
216
+ for instruction in block.instructions:
217
+ if isinstance(instruction, SetAttr):
218
+ self.store(state, instruction.object, ATTRIBUTES, state.get(instruction.value, Taint.none()))
219
+ elif isinstance(instruction, SetItem):
220
+ self.store(state, instruction.object, ELEMENTS, state.get(instruction.value, Taint.none()))
221
+ if instruction.result is None:
222
+ continue
223
+ if isinstance(instruction, Phi):
224
+ taint = Taint.none()
225
+ for value, predecessor in instruction.incoming:
226
+ if predecessor in incoming:
227
+ taint = taint.join(incoming[predecessor].get(value, Taint.none()))
228
+ elif isinstance(instruction, Call):
229
+ taint = self.call(instruction, state, flows)
230
+ else:
231
+ taint = self.instruction(instruction, state)
232
+ if isinstance(instruction, GetAttr | GetItem | GetIter):
233
+ taint = taint.join(self.loaded(instruction, state))
234
+ state[instruction.result] = taint
235
+ terminator = block.terminator
236
+ if isinstance(terminator, ForNext) and terminator.result is not None:
237
+ state[terminator.result] = self.deep(terminator.iterator, state)
238
+ return MappingProxyType(state), flows
239
+
240
+ def flow(self, cfg: CFG, block_id: BlockId, incoming: Mapping[BlockId, State]) -> Mapping[BlockId, State]:
241
+ block = self.blocks[block_id]
242
+ state, _ = self.evaluate(block, incoming)
243
+ exits = {target: state for target in block.exception_targets}
244
+ terminator = block.terminator
245
+ if isinstance(terminator, Branch):
246
+ return {**exits, terminator.then_block: state, terminator.else_block: state}
247
+ if isinstance(terminator, Jump):
248
+ return {**exits, terminator.target: state}
249
+ if isinstance(terminator, ForNext):
250
+ return {**exits, terminator.body: state, terminator.exit: state}
251
+ return exits
252
+
253
+ # ------------------------------------------------------------------ transfer
254
+
255
+ def instruction(self, instruction: Instruction, state: Mapping[Key, Taint]) -> Taint:
256
+ taint = Taint.none()
257
+ symbol = self.symbols.get(instruction.result) if instruction.result else None
258
+ if symbol is not None:
259
+ source = self.models.source_covering(symbol)
260
+ if source is not None:
261
+ taint = Taint(source.kinds, frozenset({source}))
262
+ if isinstance(instruction, Symbol | Compare):
263
+ return taint
264
+ for operand in instruction.operands():
265
+ taint = taint.join(state.get(operand, Taint.none()))
266
+ if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet):
267
+ # ``[*xs]`` and ``{**d}`` copy the contents of what they unpack.
268
+ for unpacked in instruction.unpacked:
269
+ taint = taint.join(self.deep(unpacked, state))
270
+ return taint
271
+
272
+ def call(self, call: Call, state: dict[Key, Taint], flows: list[TaintFlow]) -> Taint:
273
+ arguments = tuple(self.deep(a, state) for a in call.arguments)
274
+ keywords = Taint.none()
275
+ for value in (*call.starred, *(v for _, v in call.keywords)):
276
+ keywords = keywords.join(self.deep(value, state))
277
+ everything = keywords
278
+ for taint in arguments:
279
+ everything = everything.join(taint)
280
+ receiver = mutated_by(call, self.defs)
281
+ if receiver is not None:
282
+ self.store(state, receiver, ELEMENTS, everything)
283
+
284
+ target = self.graph.target_at(self.name, call.location)
285
+ if isinstance(target, ExternalSymbol):
286
+ project = self.project.summary(target.symbol)
287
+ if project is None:
288
+ # ``App(x)`` with ``App`` defined in another file: its ``__init__``.
289
+ project = self.project.summary(target.symbol.attribute("__init__"))
290
+ if project is not None:
291
+ through = target.symbol.canonical_name.removeprefix("python.")
292
+ bound = self.receiver(call, project.name)
293
+ arguments = (*(self.deep(r, state) for r in bound), *arguments)
294
+ return self.known(
295
+ project, through, arguments, keywords, everything, call, flows, state, (), bound
296
+ )
297
+ symbol = target.symbol
298
+ if not self.modelled(symbol):
299
+ # ``get_conn().execute`` derived ``app.database.get_conn.execute``; what
300
+ # the project function returns says what ``execute`` really is.
301
+ symbol = self.returned_symbol(call) or symbol
302
+ return self.external(symbol, everything, call, state, flows)
303
+ if isinstance(target, KnownFunction):
304
+ summary = self.summaries.summary(target.name)
305
+ captured = self.captured(call)
306
+ bound = self.receiver(call, target.name)
307
+ arguments = (
308
+ *(self.deep(r, state) for r in bound),
309
+ *arguments,
310
+ *(self.deep(value, state) for value in captured),
311
+ )
312
+ return self.known(
313
+ summary, target.name, arguments, keywords, everything, call, flows, state, captured, bound
314
+ )
315
+ returned = self.returned_symbol(call)
316
+ if returned is not None:
317
+ return self.external(returned, everything, call, state, flows)
318
+ return everything.join(state.get(call.callee, Taint.none()))
319
+
320
+ def receiver(self, call: Call, name: str) -> tuple[Value, ...]:
321
+ """The object a method call runs on, its implicit first parameter: the receiver
322
+ of ``obj.method(...)``, or the new object of ``Class(...)``."""
323
+
324
+ if "." not in name:
325
+ return ()
326
+ callee = self.defs.get(call.callee)
327
+ if isinstance(callee, GetAttr):
328
+ return (callee.object,)
329
+ if name.endswith(".__init__") and isinstance(callee, Global | Symbol):
330
+ return (call.result,)
331
+ return ()
332
+
333
+ def captured(self, call: Call) -> tuple[Value, ...]:
334
+ """The values a nested callee captured, its implicit trailing parameters."""
335
+
336
+ made = self.defs.get(call.callee)
337
+ return made.captured if isinstance(made, MakeFunction) else ()
338
+
339
+ def modelled(self, symbol: SymbolId) -> bool:
340
+ return (
341
+ self.models.sink(symbol) is not None
342
+ or self.models.sanitizer(symbol) is not None
343
+ or self.models.source_covering(symbol) is not None
344
+ )
345
+
346
+ def external(
347
+ self, symbol: SymbolId, everything: Taint, call: Call, state: Mapping[Key, Taint], flows: list[TaintFlow]
348
+ ) -> Taint:
349
+ """Sinks, sanitizers and sources of a call to an external symbol."""
350
+
351
+ sink = self.models.sink(symbol)
352
+ if sink is not None:
353
+ for position, argument in enumerate(call.arguments):
354
+ self.report(flows, sink, self.deep(argument, state), argument, call, None, None, position)
355
+ for argument in (*call.starred, *(value for _, value in call.keywords)):
356
+ self.report(flows, sink, self.deep(argument, state), argument, call, None, None, None)
357
+ sanitizer = self.models.sanitizer(symbol)
358
+ if sanitizer is not None:
359
+ return everything.without(sanitizer.kinds)
360
+ # A method on a tainted object returns tainted data (``request.args.get``).
361
+ everything = everything.join(state.get(call.callee, Taint.none()))
362
+ source = self.models.source_covering(symbol)
363
+ if source is not None:
364
+ return everything.join(Taint(source.kinds, frozenset({source})))
365
+ return everything
366
+
367
+ def returned_symbol(self, call: Call) -> SymbolId | None:
368
+ """``get_db().execute(...)``: the method of what a known function returns, when
369
+ its summary says the return value is one external symbol."""
370
+
371
+ callee = self.defs.get(call.callee)
372
+ if not isinstance(callee, GetAttr):
373
+ return None
374
+ origin = self.defs.get(callee.object)
375
+ if not isinstance(origin, Call):
376
+ return None
377
+ target = self.graph.target_at(self.name, origin.location)
378
+ summary: FunctionSummary | None = None
379
+ if isinstance(target, KnownFunction):
380
+ summary = self.summaries.summary(target.name)
381
+ elif isinstance(target, ExternalSymbol):
382
+ summary = self.project.summary(target.symbol)
383
+ if summary is None:
384
+ return None
385
+ # ``getattr(g, "_database", None) or sqlite3.connect(...)``: among the symbols the
386
+ # function may return, the one the models know about is the one that matters.
387
+ for returned in sorted(summary.return_externals, key=str):
388
+ candidate = returned.attribute(callee.attribute)
389
+ if self.modelled(candidate):
390
+ return candidate
391
+ return None
392
+
393
+ def known(
394
+ self,
395
+ summary: FunctionSummary,
396
+ through: str,
397
+ arguments: tuple[Taint, ...],
398
+ keywords: Taint,
399
+ everything: Taint,
400
+ call: Call,
401
+ flows: list[TaintFlow],
402
+ state: dict[Key, Taint],
403
+ captured: tuple[Value, ...] = (),
404
+ receiver: tuple[Value, ...] = (),
405
+ ) -> Taint:
406
+ """Flows and result taint of a call to a function whose summary is known."""
407
+
408
+ if summary.unsupported:
409
+ return everything
410
+
411
+ spread = (*call.starred, *(value for _, value in call.keywords))
412
+ values = (*receiver, *call.arguments, *captured)
413
+
414
+ def mapped(deps: frozenset[int]) -> tuple[Taint, Value | None]:
415
+ taint, witness = Taint.none(), None
416
+ for index in sorted(deps):
417
+ positional = index < len(arguments)
418
+ part = arguments[index] if positional else keywords
419
+ if part and witness is None and (positional or spread):
420
+ witness = values[index] if positional else spread[0]
421
+ taint = taint.join(part)
422
+ return taint, witness
423
+
424
+ for reached in summary.external_calls:
425
+ sink = self.models.sink(reached.symbol)
426
+ if sink is None:
427
+ continue
428
+ positions: list[int | None] = [*range(len(reached.argument_dependencies)), None]
429
+ for position, deps in zip(positions, (*reached.argument_dependencies, reached.keyword_dependencies), strict=True):
430
+ taint, witness = mapped(deps)
431
+ if witness is not None:
432
+ self.report(flows, sink, taint, witness, call, through, reached.location, position)
433
+ for mutation in summary.mutations:
434
+ if mutation.parameter < len(values):
435
+ stored = mapped(mutation.dependencies)[0]
436
+ for symbol in sorted(mutation.externals, key=str):
437
+ stored_source = self.models.source(symbol)
438
+ if stored_source is not None:
439
+ stored = stored.join(Taint(stored_source.kinds, frozenset({stored_source})))
440
+ self.store(state, values[mutation.parameter], mutation.field, stored)
441
+ result = mapped(summary.return_dependencies)[0]
442
+ for symbol in sorted(summary.return_externals, key=str):
443
+ returned_source = self.models.source(symbol)
444
+ if returned_source is not None:
445
+ result = result.join(Taint(returned_source.kinds, frozenset({returned_source})))
446
+ return result
447
+
448
+ @staticmethod
449
+ def report(
450
+ flows: list[TaintFlow],
451
+ sink: Sink,
452
+ taint: Taint,
453
+ argument: Value,
454
+ call: Call,
455
+ through: str | None,
456
+ sink_location: SourceSpan | None,
457
+ position: int | None = None,
458
+ ) -> None:
459
+ reaching = taint.kinds & sink.kinds_at(position)
460
+ if not reaching:
461
+ return
462
+ for source in sorted(taint.sources, key=lambda s: str(s.symbol)):
463
+ flows.append(
464
+ TaintFlow(
465
+ source,
466
+ sink,
467
+ reaching,
468
+ argument,
469
+ call.location,
470
+ through,
471
+ call.location if sink_location is None else sink_location,
472
+ )
473
+ )
474
+
475
+
476
+ Instances = Mapping[str, tuple[SymbolId, ...]]
477
+
478
+
479
+ def factory_instances(
480
+ module: nodes.Module,
481
+ scopes: ScopeTable,
482
+ symbols: SymbolTable,
483
+ summaries: SummaryTable,
484
+ project: SummaryIndex,
485
+ ) -> Instances:
486
+ """Module-level names bound to the result of a project function whose summary
487
+ returns known symbols: ``app = create_app()`` is a ``flask.Flask`` like
488
+ ``app = Flask(__name__)``, so its decorators resolve."""
489
+
490
+ found: dict[str, tuple[SymbolId, ...]] = {}
491
+ module_scope = scopes.module_scope.id
492
+ for statement in module.body:
493
+ if isinstance(statement, nodes.Function) and statement.decorators:
494
+ # ``@click.group() def cli``: the function is what its decorator returns, so
495
+ # ``@cli.command()`` resolves to ``click.group.command``.
496
+ decorated = tuple(
497
+ s
498
+ for d in statement.decorators
499
+ if (s := symbols.resolve_expression(module_scope, d)) is not None
500
+ )
501
+ if decorated:
502
+ found[statement.name] = decorated
503
+ continue
504
+ if not (
505
+ isinstance(statement, nodes.Assign)
506
+ and isinstance(statement.target, nodes.Name)
507
+ and isinstance(statement.value, nodes.Call)
508
+ ):
509
+ continue
510
+ callee = statement.value.callee
511
+ externals: frozenset[SymbolId] = frozenset()
512
+ if isinstance(callee, nodes.Name) and callee.identifier in summaries.names:
513
+ externals = summaries.summary(callee.identifier).return_externals
514
+ else:
515
+ symbol = symbols.resolve_expression(module_scope, callee)
516
+ summary = project.summary(symbol) if symbol is not None else None
517
+ if summary is not None:
518
+ externals = summary.return_externals
519
+ if externals:
520
+ found[statement.target.identifier] = tuple(sorted(externals, key=str))
521
+ return found
522
+
523
+
524
+ def local_instances(function: nodes.Function, scopes: ScopeTable, symbols: SymbolTable) -> Instances:
525
+ """Locals of ``function`` bound to the result of calling a resolvable symbol."""
526
+
527
+ scope = scopes.scope_for(function).id
528
+ found: dict[str, tuple[SymbolId, ...]] = {}
529
+ for statement in function.body:
530
+ if (
531
+ isinstance(statement, nodes.Assign)
532
+ and isinstance(statement.target, nodes.Name)
533
+ and isinstance(statement.value, nodes.Call)
534
+ ):
535
+ symbol = symbols.resolve_expression(scope, statement.value.callee)
536
+ if symbol is not None:
537
+ found[statement.target.identifier] = (symbol,)
538
+ return found
539
+
540
+
541
+ def _enclosing(module: nodes.Module, function: nodes.Function) -> nodes.Function | None:
542
+ """The function whose body defines ``function``, if it is nested."""
543
+
544
+ def search(body: tuple[nodes.Statement, ...], parent: nodes.Function | None) -> nodes.Function | None:
545
+ for statement in body:
546
+ if isinstance(statement, nodes.Function):
547
+ if statement.span == function.span:
548
+ return parent
549
+ found = search(statement.body, statement)
550
+ if found is not None:
551
+ return found
552
+ elif isinstance(statement, nodes.Class):
553
+ found = search(statement.body, None)
554
+ if found is not None:
555
+ return found
556
+ return None
557
+
558
+ found = search(module.body, None)
559
+ if found is not None:
560
+ return found
561
+ # Lambdas are synthesized functions: their enclosing function is the innermost
562
+ # analysable function whose span contains theirs.
563
+ innermost: nodes.Function | None = None
564
+ for candidate in analyzable_functions(module):
565
+ if candidate.span != function.span and _contains(candidate.span, function.span):
566
+ innermost = candidate
567
+ return innermost
568
+
569
+
570
+ def _contains(outer: SourceSpan, inner: SourceSpan) -> bool:
571
+ if outer.source_id != inner.source_id or outer.end_line is None or inner.end_line is None:
572
+ return False
573
+ return (outer.start_line, outer.start_column) <= (inner.start_line, inner.start_column) and (
574
+ inner.end_line,
575
+ inner.end_column,
576
+ ) <= (outer.end_line, outer.end_column)
577
+
578
+
579
+ def _instance_symbols(expression: nodes.Expression, instances: Instances) -> tuple[SymbolId, ...]:
580
+ """The symbols an expression rooted at a factory instance may denote."""
581
+
582
+ if isinstance(expression, nodes.Name):
583
+ return instances.get(expression.identifier, ())
584
+ if isinstance(expression, nodes.Attribute):
585
+ return tuple(s.attribute(expression.name) for s in _instance_symbols(expression.value, instances))
586
+ if isinstance(expression, nodes.Call):
587
+ return _instance_symbols(expression.callee, instances)
588
+ return ()
589
+
590
+
591
+ def entry_point_of(
592
+ function: nodes.Function,
593
+ models: ModelTable,
594
+ scopes: ScopeTable,
595
+ symbols: SymbolTable,
596
+ owner: nodes.Class | None = None,
597
+ instances: Instances | None = None,
598
+ ) -> EntryPoint | None:
599
+ """The entry-point model matching one of the function's decorators or, for a
600
+ method, one of the bases of ``owner``, if any."""
601
+
602
+ scope = scopes.scope_for(function)
603
+ enclosing = scope.parent if scope.parent is not None else scope.id
604
+ candidates = list(function.decorators)
605
+ if owner is not None:
606
+ class_scope = scopes.scope_for(owner)
607
+ outside = class_scope.parent if class_scope.parent is not None else class_scope.id
608
+ candidates.extend(owner.bases)
609
+ enclosing_of = {id(base): outside for base in owner.bases}
610
+ else:
611
+ enclosing_of = {}
612
+ for expression in candidates:
613
+ symbol = symbols.resolve_expression(enclosing_of.get(id(expression), enclosing), expression)
614
+ # ``app = create_app()`` resolves to the factory's symbol; the instance symbols
615
+ # say what the factory returns.
616
+ found = (*((symbol,) if symbol is not None else ()), *_instance_symbols(expression, instances or {}))
617
+ for candidate in found:
618
+ entry = models.entry_point(candidate)
619
+ if entry is not None:
620
+ return entry
621
+ return None
622
+
623
+
624
+ def parameter_sources(
625
+ function: nodes.Function,
626
+ module: nodes.Module,
627
+ models: ModelTable,
628
+ scopes: ScopeTable,
629
+ symbols: SymbolTable,
630
+ instances: Instances | None = None,
631
+ routes: Routes | None = None,
632
+ ) -> Mapping[int, Source]:
633
+ """The attacker-controlled parameters of ``function``, by index: every parameter of an
634
+ entry point (``self`` excepted for a method), including one registered elsewhere
635
+ (``routes``), and every parameter annotated with a typed-parameter symbol."""
636
+
637
+ owner = next(
638
+ (s for s in module.body if isinstance(s, nodes.Class) and any(f is function for f in s.body)),
639
+ None,
640
+ )
641
+ sources: dict[int, Source] = {}
642
+ enclosing_function = _enclosing(module, function)
643
+ if enclosing_function is not None:
644
+ # ``app = Flask(__name__)`` inside ``create_app``: routes defined there resolve.
645
+ instances = {**(instances or {}), **local_instances(enclosing_function, scopes, symbols)}
646
+ entry = entry_point_of(function, models, scopes, symbols, owner, instances)
647
+ if entry is None and routes:
648
+ qualified = function.name if owner is None else f"{owner.name}.{function.name}"
649
+ entry = routes.get(project_symbol(module.name, qualified))
650
+ if entry is None and owner is not None:
651
+ entry = routes.get(project_symbol(module.name, owner.name))
652
+ if entry is not None:
653
+ first = 1 if owner is not None else 0
654
+ for index in range(first, len(function.parameters)):
655
+ sources[index] = Source(entry.symbol, entry.label, entry.kinds)
656
+ scope = scopes.scope_for(function)
657
+ enclosing = scope.parent if scope.parent is not None else scope.id
658
+ for index, parameter in enumerate(function.parameters):
659
+ if parameter.annotation is None:
660
+ continue
661
+ symbol = symbols.resolve_expression(enclosing, parameter.annotation)
662
+ typed = models.typed_parameter(symbol) if symbol is not None else None
663
+ if typed is not None:
664
+ sources[index] = Source(typed.symbol, typed.label, typed.kinds)
665
+ for index, parameter in enumerate(function.parameters):
666
+ if index in sources:
667
+ continue
668
+ for named in models.named_parameters:
669
+ if named.matches(parameter.name):
670
+ sources[index] = Source(
671
+ SymbolId(f"python.parameter.{parameter.name}"), named.label, named.kinds
672
+ )
673
+ break
674
+ return sources
675
+
676
+
677
+ def propagate_taint(
678
+ name: str,
679
+ function: FunctionIR,
680
+ cfg: CFG,
681
+ models: ModelTable,
682
+ graph: CallGraph,
683
+ summaries: SummaryTable,
684
+ parameters: Mapping[int, Source] | None = None,
685
+ project: SummaryIndex | None = None,
686
+ heap: HeapFacts | None = None,
687
+ seeds: Mapping[HeapLocation, Taint] | None = None,
688
+ ) -> TaintFacts:
689
+ problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds)
690
+ solution = solve(problem, cfg)
691
+ taints: dict[Key, Taint] = {}
692
+ flows: list[TaintFlow] = []
693
+ for block in function.blocks:
694
+ if solution.reached(block.id):
695
+ state, found = problem.evaluate(block, solution.incoming(block.id))
696
+ taints.update(state)
697
+ flows.extend(found)
698
+ return TaintFacts(taints, tuple(dict.fromkeys(flows)))
699
+
700
+
701
+ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
702
+ """Shared taint result every detector consumes."""
703
+
704
+ name: ClassVar[str] = "taint.flows"
705
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
706
+ {
707
+ SSAAnalysis,
708
+ CFGAnalysis,
709
+ SecurityModelAnalysis,
710
+ CallGraphAnalysis,
711
+ SummaryAnalysis,
712
+ ScopeAnalysis,
713
+ SymbolAnalysis,
714
+ ProjectSummaries,
715
+ HeapAnalysis,
716
+ RegisteredRoutes,
717
+ }
718
+ )
719
+
720
+ @classmethod
721
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> TaintFacts:
722
+ graph = ctx.get(CallGraphAnalysis)
723
+ models = ctx.get(SecurityModelAnalysis)
724
+ scopes, symbols = ctx.get(ScopeAnalysis), ctx.get(SymbolAnalysis)
725
+ instances = factory_instances(ctx.module, scopes, symbols, ctx.get(SummaryAnalysis), ctx.get(ProjectSummaries))
726
+ routes = ctx.get(RegisteredRoutes)
727
+
728
+ def sources_of(member: nodes.Function) -> Mapping[int, Source]:
729
+ return parameter_sources(member, ctx.module, models, scopes, symbols, instances, routes)
730
+
731
+ ssa = ctx.get(SSAAnalysis, function)
732
+ heap = ctx.get(HeapAnalysis, function)
733
+ sources = sources_of(function)
734
+ # Only a method the framework calls, an entry point, starts with what its
735
+ # siblings stored; a method the project calls gets its ``self`` from the caller.
736
+ seeds = (
737
+ self_seeds(function, ctx.module, ssa, heap, models, graph, ctx.get(SummaryAnalysis), sources_of)
738
+ if sources
739
+ else {}
740
+ )
741
+ return propagate_taint(
742
+ graph.name_of(function),
743
+ ssa,
744
+ ctx.get(CFGAnalysis, function),
745
+ models,
746
+ graph,
747
+ ctx.get(SummaryAnalysis),
748
+ sources,
749
+ ctx.get(ProjectSummaries),
750
+ heap,
751
+ seeds,
752
+ )
753
+
754
+
755
+ def self_seeds(
756
+ function: nodes.Function,
757
+ module: nodes.Module,
758
+ ssa: FunctionIR,
759
+ heap: HeapFacts,
760
+ models: ModelTable,
761
+ graph: CallGraph,
762
+ summaries: SummaryTable,
763
+ sources_of: Callable[[nodes.Function], Mapping[int, Source]],
764
+ ) -> dict[HeapLocation, Taint]:
765
+ """What the sibling methods of a method store into ``self`` from their own inputs:
766
+ the attributes ``self`` starts with when the framework, not the project, calls the
767
+ methods (``self.cmd = request.POST[...]`` in ``post``, read by ``get``)."""
768
+
769
+ owner = next(
770
+ (s for s in module.body if isinstance(s, nodes.Class) and any(m is function for m in s.body)),
771
+ None,
772
+ )
773
+ if owner is None or not ssa.parameters:
774
+ return {}
775
+ seeds: dict[HeapLocation, Taint] = {}
776
+ for sibling in owner.body:
777
+ if not isinstance(sibling, nodes.Function) or sibling is function:
778
+ continue
779
+ try:
780
+ summary = summaries.summary(graph.name_of(sibling))
781
+ except KeyError:
782
+ continue
783
+ sources = sources_of(sibling)
784
+ for mutation in summary.mutations:
785
+ if mutation.parameter != 0:
786
+ continue
787
+ taint = Taint.none()
788
+ for index in mutation.dependencies:
789
+ source = sources.get(index)
790
+ if source is not None:
791
+ taint = taint.join(Taint(source.kinds, frozenset({source})))
792
+ for symbol in mutation.externals:
793
+ external = models.source(symbol)
794
+ if external is not None:
795
+ taint = taint.join(Taint(external.kinds, frozenset({external})))
796
+ if not taint:
797
+ continue
798
+ for location in heap.locations(ssa.parameters[0], mutation.field):
799
+ seeds[location] = seeds.get(location, Taint.none()).join(taint)
800
+ return seeds