coretrace-python-analyzer 0.15.0__py3-none-any.whl → 0.16.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.
- coretrace_python/__init__.py +1 -1
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +9 -2
- coretrace_python/bundled/models/django/django_models.py +4 -1
- coretrace_python/dependency/vex.py +10 -1
- coretrace_python/engine.py +27 -3
- coretrace_python/findings/coverage.py +4 -1
- coretrace_python/interprocedural/__init__.py +8 -0
- coretrace_python/interprocedural/summaries.py +173 -7
- coretrace_python/plugins/api.py +7 -4
- coretrace_python/taint/__init__.py +16 -1
- coretrace_python/taint/engine.py +63 -4
- coretrace_python/taint/models.py +20 -2
- coretrace_python/taint/templates.py +458 -18
- {coretrace_python_analyzer-0.15.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.15.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/RECORD +19 -19
- {coretrace_python_analyzer-0.15.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.15.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.15.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.15.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Calls, anywhere in the project, to an API affected by a vulnerable requirement, and
|
|
2
|
-
reads of an affected attribute whose getter runs the vulnerable code.
|
|
2
|
+
reads of an affected attribute whose getter runs the vulnerable code. A filter a project
|
|
3
|
+
template applies is a call to the function behind it, at the template's line."""
|
|
3
4
|
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
@@ -56,6 +57,10 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
56
57
|
reported.add((advisory, symbol))
|
|
57
58
|
check = check_conditions(entry, None)
|
|
58
59
|
findings.append(_reached(advisory, symbol, read.location, function, check))
|
|
60
|
+
for call in ctx.templates.calls:
|
|
61
|
+
for advisory in affected.get(call.symbol, ()):
|
|
62
|
+
check = check_conditions(advisory.entry_point(call.symbol), None)
|
|
63
|
+
findings.append(_reached(advisory, call.symbol, call.span, None, check))
|
|
59
64
|
return findings
|
|
60
65
|
|
|
61
66
|
|
|
@@ -67,7 +72,9 @@ def _read_through(symbol: SymbolId) -> Iterator[SymbolId]:
|
|
|
67
72
|
yield SymbolId(".".join(components[:end]))
|
|
68
73
|
|
|
69
74
|
|
|
70
|
-
def _reached(
|
|
75
|
+
def _reached(
|
|
76
|
+
advisory: Advisory, symbol: SymbolId, span: SourceSpan, function: str | None, check: ConditionCheck
|
|
77
|
+
) -> Finding:
|
|
71
78
|
return Finding(
|
|
72
79
|
rule_id="reachable-vulnerability",
|
|
73
80
|
message=(
|
|
@@ -139,7 +139,10 @@ class DjangoModels(ModelPlugin):
|
|
|
139
139
|
Sink(_sym("django.http.HttpResponseRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
140
140
|
Sink(_sym("django.http.HttpResponsePermanentRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
141
141
|
Sanitizer(_sym("django.utils.html.escape"), TaintKind.HTML),
|
|
142
|
-
TemplateRender(_sym("django.template.loader.render_to_string")),
|
|
142
|
+
TemplateRender(_sym("django.template.loader.render_to_string"), request=2),
|
|
143
|
+
TemplateRender(_sym("django.shortcuts.render"), 1, context=2, request=0),
|
|
144
|
+
TemplateRender(_sym("django.template.response.TemplateResponse"), 1, "template", 2, 0),
|
|
145
|
+
TemplateRender(_sym("django.template.response.SimpleTemplateResponse"), 0, "template"),
|
|
143
146
|
Sanitizer(_sym("django.utils.html.conditional_escape"), TaintKind.HTML),
|
|
144
147
|
# The masked CSRF secret: ASCII letters and digits only, a malformed cookie is
|
|
145
148
|
# replaced before it is used.
|
|
@@ -7,7 +7,8 @@ subcomponent of the project:
|
|
|
7
7
|
exploitable finding, whatever the policy or a suppression did with it;
|
|
8
8
|
- ``not_affected``, as ``vulnerable_code_not_in_execute_path``, only when the advisory is
|
|
9
9
|
curated (it names entry points), no code of the project reaches them, every file and
|
|
10
|
-
function was analysed,
|
|
10
|
+
function was analysed, every template the project names was read when a template
|
|
11
|
+
filter can reach them, and a lock file shows that no other package requires the
|
|
11
12
|
vulnerable one, since the code of installed packages is not analysed;
|
|
12
13
|
- ``under_investigation`` otherwise, with the reason in its notes.
|
|
13
14
|
"""
|
|
@@ -25,6 +26,7 @@ from coretrace_python.dependency.graph import Advisory, DependencyGraph, Require
|
|
|
25
26
|
from coretrace_python.dependency.sbom import purl
|
|
26
27
|
from coretrace_python.findings import Component, Finding
|
|
27
28
|
from coretrace_python.findings.coverage import Coverage
|
|
29
|
+
from coretrace_python.taint import FILTER_FUNCTIONS
|
|
28
30
|
|
|
29
31
|
CONTEXT = "https://openvex.dev/ns/v0.2.0"
|
|
30
32
|
# OpenVEX's shared namespace for documents without an IRI of their own, and the author
|
|
@@ -33,6 +35,8 @@ NAMESPACE = "https://openvex.dev/docs/public/vex-"
|
|
|
33
35
|
AUTHOR = "Unknown Author"
|
|
34
36
|
# Evidence levels that put the vulnerable code in the project's execution path, lowest first.
|
|
35
37
|
REACHED = ("reachable", "exploitable")
|
|
38
|
+
# What a template calls: a template the engine cannot read may reach these.
|
|
39
|
+
_TEMPLATE_CALLS = frozenset(FILTER_FUNCTIONS.values())
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def render_vex(
|
|
@@ -140,6 +144,11 @@ def _status(
|
|
|
140
144
|
f"No analysed code reaches {entries}, but {', '.join(partial)} could not be fully "
|
|
141
145
|
f"analysed.{ruled}"
|
|
142
146
|
)
|
|
147
|
+
if coverage.unread_templates and _TEMPLATE_CALLS.intersection(advisory.reachable_symbols):
|
|
148
|
+
return _investigating(
|
|
149
|
+
f"No analysed code reaches {entries}, but a template can reach them, and the engine could "
|
|
150
|
+
f"not read every template the project names: {'; '.join(coverage.unread_templates)}.{ruled}"
|
|
151
|
+
)
|
|
143
152
|
required_by = dependencies.required_by(package)
|
|
144
153
|
if required_by is None:
|
|
145
154
|
return _investigating(
|
coretrace_python/engine.py
CHANGED
|
@@ -83,6 +83,8 @@ from coretrace_python.interprocedural import (
|
|
|
83
83
|
SummaryAnalysis,
|
|
84
84
|
SummaryIndex,
|
|
85
85
|
SymbolRead,
|
|
86
|
+
TemplateCalls,
|
|
87
|
+
TemplateCallsAnalysis,
|
|
86
88
|
build_module_graph,
|
|
87
89
|
discover_sources,
|
|
88
90
|
project_symbol,
|
|
@@ -124,7 +126,10 @@ from coretrace_python.taint import (
|
|
|
124
126
|
TaintAnalysis,
|
|
125
127
|
TaintKind,
|
|
126
128
|
escaped_templates,
|
|
129
|
+
project_templates,
|
|
127
130
|
registered_routes,
|
|
131
|
+
request_processor,
|
|
132
|
+
unread_renders,
|
|
128
133
|
)
|
|
129
134
|
from coretrace_python.taint.urls import flow_url
|
|
130
135
|
|
|
@@ -158,6 +163,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
|
|
|
158
163
|
RegisteredRoutes,
|
|
159
164
|
EscapedTemplates,
|
|
160
165
|
ClearingAnalysis,
|
|
166
|
+
TemplateCallsAnalysis,
|
|
161
167
|
)
|
|
162
168
|
|
|
163
169
|
|
|
@@ -198,6 +204,7 @@ class ResultsEvicted(TransformationPass):
|
|
|
198
204
|
RegisteredRoutes,
|
|
199
205
|
EscapedTemplates,
|
|
200
206
|
ClearingAnalysis,
|
|
207
|
+
TemplateCallsAnalysis,
|
|
201
208
|
}
|
|
202
209
|
)
|
|
203
210
|
|
|
@@ -431,13 +438,20 @@ def analyze_project(
|
|
|
431
438
|
for symbol, registered in _routes_of(analysable[name]).items():
|
|
432
439
|
routes.setdefault(symbol, registered)
|
|
433
440
|
escaped = escaped_templates(root)
|
|
441
|
+
templates = project_templates(root)
|
|
434
442
|
clearing = models.clearing(escaped)
|
|
443
|
+
template_calls = models.template_calls(
|
|
444
|
+
templates.filters, request_processor(modules[name] for name in sorted(analysable))
|
|
445
|
+
)
|
|
435
446
|
for manager in analysable.values():
|
|
436
447
|
manager.provide(RegisteredRoutes, routes)
|
|
437
448
|
manager.provide(EscapedTemplates, escaped)
|
|
438
449
|
manager.provide(ClearingAnalysis, clearing)
|
|
450
|
+
manager.provide(TemplateCallsAnalysis, template_calls)
|
|
439
451
|
|
|
440
|
-
configuration = _configuration_key(
|
|
452
|
+
configuration = _configuration_key(
|
|
453
|
+
components, plugins, models, advisories, dependencies, routes, escaped, template_calls
|
|
454
|
+
)
|
|
441
455
|
keys = module_keys(
|
|
442
456
|
graph,
|
|
443
457
|
{name: fingerprint(configuration, str(files[name].source_id), name, files[name].text) for name in analysable},
|
|
@@ -479,6 +493,7 @@ def analyze_project(
|
|
|
479
493
|
advisory_paths,
|
|
480
494
|
_encode_routes(routes),
|
|
481
495
|
tuple(sorted(escaped)),
|
|
496
|
+
template_calls,
|
|
482
497
|
),
|
|
483
498
|
)
|
|
484
499
|
for component in pending
|
|
@@ -516,7 +531,9 @@ def analyze_project(
|
|
|
516
531
|
frozenset(),
|
|
517
532
|
reads={f: tuple(r) for f, r in reads.items()},
|
|
518
533
|
)
|
|
519
|
-
context = ProjectContext(
|
|
534
|
+
context = ProjectContext(
|
|
535
|
+
graph, dependencies, advisories, analysable, call_graphs, policy, root, functions, templates
|
|
536
|
+
)
|
|
520
537
|
for plugin in all_plugins:
|
|
521
538
|
if isinstance(plugin, ProjectPlugin):
|
|
522
539
|
findings.extend(plugin.analyze_project(context))
|
|
@@ -526,6 +543,9 @@ def analyze_project(
|
|
|
526
543
|
findings = [_sourced(finding, origins) for finding in findings]
|
|
527
544
|
accepted = tuple(f for f in findings if policy.accepts(f))
|
|
528
545
|
kept, suppressed = partition(apply_policy(policy, findings), _text_of(sources))
|
|
546
|
+
unread = list(templates.unread)
|
|
547
|
+
for name in sorted(call_graphs):
|
|
548
|
+
unread.extend(unread_renders(name, call_graphs[name], models, templates.names))
|
|
529
549
|
return ProjectAnalysis(
|
|
530
550
|
graph,
|
|
531
551
|
index,
|
|
@@ -534,7 +554,7 @@ def analyze_project(
|
|
|
534
554
|
MappingProxyType(keys),
|
|
535
555
|
reused,
|
|
536
556
|
advisories,
|
|
537
|
-
Coverage(tuple(sorted(coverage, key=lambda c: c.path))),
|
|
557
|
+
Coverage(tuple(sorted(coverage, key=lambda c: c.path)), tuple(unread)),
|
|
538
558
|
suppressed,
|
|
539
559
|
accepted,
|
|
540
560
|
components,
|
|
@@ -686,6 +706,7 @@ class _Batch:
|
|
|
686
706
|
advisory_paths: tuple[Path, ...] = ()
|
|
687
707
|
routes: tuple[tuple[str, str, str, int], ...] = ()
|
|
688
708
|
escaped: tuple[str, ...] = ()
|
|
709
|
+
template_calls: TemplateCalls = field(default_factory=TemplateCalls)
|
|
689
710
|
|
|
690
711
|
|
|
691
712
|
def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
|
|
@@ -713,6 +734,7 @@ def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
|
|
|
713
734
|
manager.provide(RegisteredRoutes, routes)
|
|
714
735
|
manager.provide(EscapedTemplates, escaped)
|
|
715
736
|
manager.provide(ClearingAnalysis, models.clearing(escaped))
|
|
737
|
+
manager.provide(TemplateCallsAnalysis, batch.template_calls)
|
|
716
738
|
module_plugins = tuple(p for p in all_plugins if not isinstance(p, ProjectPlugin))
|
|
717
739
|
results = _analyse_managers(managers, decode_index(batch.seed), module_plugins, affected)
|
|
718
740
|
return {name: encode(entry) for name, entry in results.items()}
|
|
@@ -741,6 +763,7 @@ def _configuration_key(
|
|
|
741
763
|
dependencies: DependencyGraph,
|
|
742
764
|
routes: Routes | None = None,
|
|
743
765
|
escaped: frozenset[str] = frozenset(),
|
|
766
|
+
template_calls: TemplateCalls | None = None,
|
|
744
767
|
) -> str:
|
|
745
768
|
"""Everything a module's results depend on besides the project sources (§11)."""
|
|
746
769
|
|
|
@@ -758,6 +781,7 @@ def _configuration_key(
|
|
|
758
781
|
repr(dependencies.errors),
|
|
759
782
|
repr(_encode_routes(routes or {})),
|
|
760
783
|
repr(sorted(escaped)),
|
|
784
|
+
repr(template_calls),
|
|
761
785
|
)
|
|
762
786
|
|
|
763
787
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
"No findings" only means something when the reader knows what was analysed. Each file
|
|
4
4
|
is ``analysed``, a ``syntax-error`` (the frontend rejected it) or ``unreadable`` (it could
|
|
5
|
-
not be decoded); analysed files count their functions and how many lowered.
|
|
5
|
+
not be decoded); analysed files count their functions and how many lowered. The
|
|
6
|
+
templates the project names but the run could not read are listed, each where the
|
|
7
|
+
project names it: they may call any template filter.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -21,6 +23,7 @@ class FileCoverage:
|
|
|
21
23
|
@dataclass(frozen=True)
|
|
22
24
|
class Coverage:
|
|
23
25
|
details: tuple[FileCoverage, ...] = ()
|
|
26
|
+
unread_templates: tuple[str, ...] = ()
|
|
24
27
|
|
|
25
28
|
@property
|
|
26
29
|
def files(self) -> int:
|
|
@@ -20,6 +20,7 @@ from coretrace_python.interprocedural.modulegraph import (
|
|
|
20
20
|
project_symbol,
|
|
21
21
|
)
|
|
22
22
|
from coretrace_python.interprocedural.summaries import (
|
|
23
|
+
FILTER_ARGUMENTS,
|
|
23
24
|
Cleared,
|
|
24
25
|
Clearing,
|
|
25
26
|
ClearingAnalysis,
|
|
@@ -31,10 +32,14 @@ from coretrace_python.interprocedural.summaries import (
|
|
|
31
32
|
SummaryAnalysis,
|
|
32
33
|
SummaryIndex,
|
|
33
34
|
SummaryTable,
|
|
35
|
+
TemplateCalls,
|
|
36
|
+
TemplateCallsAnalysis,
|
|
37
|
+
TemplateFilter,
|
|
34
38
|
cleared_by,
|
|
35
39
|
)
|
|
36
40
|
|
|
37
41
|
__all__ = [
|
|
42
|
+
"FILTER_ARGUMENTS",
|
|
38
43
|
"Arguments",
|
|
39
44
|
"CallGraph",
|
|
40
45
|
"CallGraphAnalysis",
|
|
@@ -56,6 +61,9 @@ __all__ = [
|
|
|
56
61
|
"SummaryTable",
|
|
57
62
|
"SymbolRead",
|
|
58
63
|
"Target",
|
|
64
|
+
"TemplateCalls",
|
|
65
|
+
"TemplateCallsAnalysis",
|
|
66
|
+
"TemplateFilter",
|
|
59
67
|
"UnknownTarget",
|
|
60
68
|
"build_module_graph",
|
|
61
69
|
"cleared_by",
|
|
@@ -7,14 +7,17 @@ over the call graph, so recursion converges to the least solution. Summaries car
|
|
|
7
7
|
security knowledge of their own: the taint engine decides which external symbols
|
|
8
8
|
matter. They only keep, per parameter, the bits a ``Clearing`` the engine provides says
|
|
9
9
|
the calls on the way cleared, on every path, so a caller's data reaches a sink or comes
|
|
10
|
-
back without the kinds a sanitizer inside the function removed.
|
|
10
|
+
back without the kinds a sanitizer inside the function removed. A call rendering a
|
|
11
|
+
template makes the filter calls ``TemplateCalls`` says the template makes, with the
|
|
12
|
+
values of the context entries it reads.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
14
16
|
|
|
15
17
|
import dataclasses
|
|
16
|
-
from collections.abc import Iterable, Mapping
|
|
18
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
17
19
|
from dataclasses import dataclass
|
|
20
|
+
from functools import cached_property
|
|
18
21
|
from types import MappingProxyType
|
|
19
22
|
from typing import ClassVar, TypeVar
|
|
20
23
|
|
|
@@ -38,6 +41,7 @@ from coretrace_python.interprocedural.callgraph import (
|
|
|
38
41
|
UnknownTarget,
|
|
39
42
|
_is_staticmethod,
|
|
40
43
|
)
|
|
44
|
+
from coretrace_python.ir.defuse import DefUse, def_use
|
|
41
45
|
from coretrace_python.ir.model import (
|
|
42
46
|
BasicBlock,
|
|
43
47
|
Branch,
|
|
@@ -145,6 +149,131 @@ class Clearing:
|
|
|
145
149
|
return bits
|
|
146
150
|
|
|
147
151
|
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class TemplateFilter:
|
|
154
|
+
"""A filter call rendering a template makes: the function behind the filter, where
|
|
155
|
+
it is applied, in the template named ``template``, and the variables of the render,
|
|
156
|
+
each with its first attribute (``bio``, ``request.GET``), whose values reach its value
|
|
157
|
+
and its argument. They are sorted: the calls are part of the cache key."""
|
|
158
|
+
|
|
159
|
+
symbol: SymbolId
|
|
160
|
+
location: SourceSpan
|
|
161
|
+
template: str
|
|
162
|
+
value: tuple[str, ...] = ()
|
|
163
|
+
argument: tuple[str, ...] = ()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# What the analysis knows of the arguments of a filter call: nothing, so no condition on
|
|
167
|
+
# them is decided.
|
|
168
|
+
FILTER_ARGUMENTS = Arguments(unpacked=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class Rendering:
|
|
173
|
+
"""What a render call gives the template: the filter calls it makes, the context
|
|
174
|
+
``entries`` by key, and the ``request`` the request context processor adds, if it
|
|
175
|
+
certainly does. An entry of that name shadows the request."""
|
|
176
|
+
|
|
177
|
+
filters: tuple[TemplateFilter, ...]
|
|
178
|
+
entries: Mapping[str, Value]
|
|
179
|
+
request: Value | None = None
|
|
180
|
+
|
|
181
|
+
def values(self, names: tuple[str, ...]) -> Iterator[tuple[Value, str | None]]:
|
|
182
|
+
"""The values reaching a filter through ``names``, each a variable with its first
|
|
183
|
+
attribute: a context entry, or the request with the attribute read from it."""
|
|
184
|
+
|
|
185
|
+
for name in names:
|
|
186
|
+
variable, _, attribute = name.partition(".")
|
|
187
|
+
if variable in self.entries:
|
|
188
|
+
yield self.entries[variable], None
|
|
189
|
+
elif variable == REQUEST and self.request is not None and attribute:
|
|
190
|
+
yield self.request, attribute
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# The variable the request context processor adds.
|
|
194
|
+
REQUEST = "request"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class TemplateCalls:
|
|
199
|
+
"""The filter calls a render makes, provided by the engine from the project's
|
|
200
|
+
templates: ``renders`` gives, for each render symbol, the position and keyword of
|
|
201
|
+
the template's name, the position of its ``context`` and that of its ``request``
|
|
202
|
+
(None when it takes none); ``filters`` the calls rendering each template makes, by
|
|
203
|
+
its name the way call sites record a constant; ``request`` says the request context
|
|
204
|
+
processor certainly runs for every template."""
|
|
205
|
+
|
|
206
|
+
renders: Mapping[SymbolId, tuple[int, str, int, int | None]] = dataclasses.field(default_factory=dict)
|
|
207
|
+
filters: Mapping[str, tuple[TemplateFilter, ...]] = dataclasses.field(default_factory=dict)
|
|
208
|
+
request: bool = False
|
|
209
|
+
|
|
210
|
+
def rendering(
|
|
211
|
+
self, symbol: SymbolId, call: Call, arguments: Arguments, defs: Mapping[Value, Instruction], uses: DefUse
|
|
212
|
+
) -> Rendering | None:
|
|
213
|
+
"""What ``call`` gives the template it names, when it renders one whose filter
|
|
214
|
+
calls are known. None unless the context is certain: absent, or a dict literal
|
|
215
|
+
with constant keys that ``call`` alone uses; any other context may hold other
|
|
216
|
+
values, or a ``request`` entry, by the time the template reads it."""
|
|
217
|
+
|
|
218
|
+
render = self.renders.get(symbol)
|
|
219
|
+
if render is None:
|
|
220
|
+
return None
|
|
221
|
+
position, keyword, context, request = render
|
|
222
|
+
named = arguments.given(keyword, position)
|
|
223
|
+
filters = self.filters.get(named[1] or "", ()) if named is not None and named[0] else ()
|
|
224
|
+
passed = _passed(call, context, "context") if filters else None
|
|
225
|
+
entries = _literal_entries(passed[1], defs, uses) if passed is not None and passed[0] else None
|
|
226
|
+
if entries is None:
|
|
227
|
+
return None
|
|
228
|
+
given = _passed(call, request, REQUEST) if self.request and request is not None else None
|
|
229
|
+
return Rendering(filters, entries, given[1] if given is not None and given[0] else None)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _passed(call: Call, position: int, keyword: str) -> tuple[bool, Value | None]:
|
|
233
|
+
"""Whether what ``call`` passes at ``position`` or as ``keyword`` is certain, and
|
|
234
|
+
that value: None when the call passes none."""
|
|
235
|
+
|
|
236
|
+
if call.starred or any(name is None for name, _ in call.keywords):
|
|
237
|
+
return False, None
|
|
238
|
+
if position < len(call.arguments):
|
|
239
|
+
return True, call.arguments[position]
|
|
240
|
+
return True, next((value for name, value in call.keywords if name == keyword), None)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _is_none(value: Value | None, defs: Mapping[Value, Instruction]) -> bool:
|
|
244
|
+
constant = defs.get(value) if value is not None else None
|
|
245
|
+
return value is None or (isinstance(constant, Constant) and constant.value is None)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _literal_entries(value: Value | None, defs: Mapping[Value, Instruction], uses: DefUse) -> Mapping[str, Value] | None:
|
|
249
|
+
"""The entries of a context, by key: none for an absent context, None unless it is a
|
|
250
|
+
dict literal with constant keys that nothing else uses."""
|
|
251
|
+
|
|
252
|
+
if _is_none(value, defs):
|
|
253
|
+
return {}
|
|
254
|
+
built = defs.get(value) if value is not None else None
|
|
255
|
+
if value is None or not isinstance(built, BuildDict) or built.unpacked or len(uses.uses(value)) != 1:
|
|
256
|
+
return None
|
|
257
|
+
entries: dict[str, Value] = {}
|
|
258
|
+
for key, item in built.items:
|
|
259
|
+
constant = defs.get(key)
|
|
260
|
+
if not isinstance(constant, Constant) or not isinstance(constant.value, str):
|
|
261
|
+
return None
|
|
262
|
+
entries[constant.value] = item
|
|
263
|
+
return entries
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class TemplateCallsAnalysis(Analysis[TemplateCalls]):
|
|
267
|
+
"""The filter calls renders make, provided by the engine for a project; none on its
|
|
268
|
+
own."""
|
|
269
|
+
|
|
270
|
+
name: ClassVar[str] = "interprocedural.template_calls"
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def compute(cls, ctx: AnalysisContext) -> TemplateCalls:
|
|
274
|
+
return TemplateCalls()
|
|
275
|
+
|
|
276
|
+
|
|
148
277
|
class ClearingAnalysis(Analysis[Clearing]):
|
|
149
278
|
"""What calls clear, provided by the engine from the security models; nothing on its
|
|
150
279
|
own, so a summary then keeps every kind."""
|
|
@@ -278,6 +407,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
278
407
|
project: Mapping[SymbolId, FunctionSummary] | None = None,
|
|
279
408
|
heap: HeapFacts | None = None,
|
|
280
409
|
clearing: Clearing | None = None,
|
|
410
|
+
templates: TemplateCalls | None = None,
|
|
281
411
|
) -> None:
|
|
282
412
|
self.name = name
|
|
283
413
|
self.function = function
|
|
@@ -286,6 +416,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
286
416
|
self.project = project or {}
|
|
287
417
|
self.heap = heap or HeapFacts({})
|
|
288
418
|
self.clearing = clearing or Clearing()
|
|
419
|
+
self.templates = templates or TemplateCalls()
|
|
289
420
|
self.blocks = {block.id: block for block in function.blocks}
|
|
290
421
|
self.defs: dict[Value, Instruction] = {
|
|
291
422
|
i.result: i for block in function.blocks for i in block.instructions if i.result
|
|
@@ -428,6 +559,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
428
559
|
return self.known(project, arguments, keywords, everything, call, state, bound)
|
|
429
560
|
given = self.graph.arguments_at(self.name, call.location)
|
|
430
561
|
self.record(target.symbol, arguments, named, call.location, None, given)
|
|
562
|
+
self.record_filters(target.symbol, call, given, state)
|
|
431
563
|
cleared = everything.clearing(self.clearing.clears(target.symbol, given))
|
|
432
564
|
return cleared | Dep(externals=frozenset({target.symbol}))
|
|
433
565
|
if isinstance(target, KnownFunction):
|
|
@@ -521,6 +653,31 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
521
653
|
self.store(state, values[mutation.parameter], mutation.field, deps)
|
|
522
654
|
return mapped(callee.return_dependencies, callee.return_cleared) | Dep(externals=callee.return_externals)
|
|
523
655
|
|
|
656
|
+
@cached_property
|
|
657
|
+
def uses(self) -> DefUse:
|
|
658
|
+
return def_use(self.function)
|
|
659
|
+
|
|
660
|
+
def record_filters(self, symbol: SymbolId, call: Call, given: Arguments, state: Mapping[Key, Dep]) -> None:
|
|
661
|
+
"""The filter calls a render makes, each at the filter in its template, with what
|
|
662
|
+
the context entries reaching its value and its argument depend on. The request the
|
|
663
|
+
request context processor adds is not followed through a project function."""
|
|
664
|
+
|
|
665
|
+
rendering = self.templates.rendering(symbol, call, given, self.defs, self.uses)
|
|
666
|
+
if rendering is None:
|
|
667
|
+
return
|
|
668
|
+
for template_filter in rendering.filters:
|
|
669
|
+
passed: list[Dep] = []
|
|
670
|
+
for names in (template_filter.value, template_filter.argument):
|
|
671
|
+
deps = EMPTY
|
|
672
|
+
for value, attribute in rendering.values(names):
|
|
673
|
+
# Only the kinds of a request object say which of its attributes the
|
|
674
|
+
# user controls, and a dependency carries no kinds.
|
|
675
|
+
if attribute is None:
|
|
676
|
+
deps |= self.deep(value, state)
|
|
677
|
+
passed.append(deps)
|
|
678
|
+
if any(deps.parameters for deps in passed):
|
|
679
|
+
self.record(template_filter.symbol, tuple(passed), (), template_filter.location, None, FILTER_ARGUMENTS)
|
|
680
|
+
|
|
524
681
|
def record(
|
|
525
682
|
self,
|
|
526
683
|
symbol: SymbolId,
|
|
@@ -570,8 +727,9 @@ def summarize(
|
|
|
570
727
|
heap: HeapFacts | None = None,
|
|
571
728
|
static: bool = False,
|
|
572
729
|
clearing: Clearing | None = None,
|
|
730
|
+
templates: TemplateCalls | None = None,
|
|
573
731
|
) -> FunctionSummary:
|
|
574
|
-
problem = _DependenceProblem(name, function, graph, table, project, heap, clearing)
|
|
732
|
+
problem = _DependenceProblem(name, function, graph, table, project, heap, clearing, templates)
|
|
575
733
|
solution = solve(problem, cfg)
|
|
576
734
|
problem.external = {}
|
|
577
735
|
problem.returns = EMPTY
|
|
@@ -610,7 +768,15 @@ def summarize(
|
|
|
610
768
|
class SummaryAnalysis(Analysis[SummaryTable]):
|
|
611
769
|
name: ClassVar[str] = "interprocedural.summaries"
|
|
612
770
|
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
|
|
613
|
-
{
|
|
771
|
+
{
|
|
772
|
+
CallGraphAnalysis,
|
|
773
|
+
SSAAnalysis,
|
|
774
|
+
CFGAnalysis,
|
|
775
|
+
ProjectSummaries,
|
|
776
|
+
HeapAnalysis,
|
|
777
|
+
ClearingAnalysis,
|
|
778
|
+
TemplateCallsAnalysis,
|
|
779
|
+
}
|
|
614
780
|
)
|
|
615
781
|
|
|
616
782
|
@classmethod
|
|
@@ -618,6 +784,7 @@ class SummaryAnalysis(Analysis[SummaryTable]):
|
|
|
618
784
|
graph = ctx.get(CallGraphAnalysis)
|
|
619
785
|
project = ctx.get(ProjectSummaries).summaries
|
|
620
786
|
clearing = ctx.get(ClearingAnalysis)
|
|
787
|
+
templates = ctx.get(TemplateCallsAnalysis)
|
|
621
788
|
table: dict[str, FunctionSummary] = {}
|
|
622
789
|
supported: dict[str, tuple[FunctionIR, CFG, HeapFacts]] = {}
|
|
623
790
|
for name, function in graph.definitions.items():
|
|
@@ -635,9 +802,8 @@ class SummaryAnalysis(Analysis[SummaryTable]):
|
|
|
635
802
|
while changed:
|
|
636
803
|
changed = False
|
|
637
804
|
for name, (ssa, cfg, heap) in supported.items():
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
)
|
|
805
|
+
static = _is_staticmethod(graph.definitions[name])
|
|
806
|
+
updated = summarize(name, ssa, cfg, graph, table, project, heap, static, clearing, templates)
|
|
641
807
|
if updated != table[name]:
|
|
642
808
|
table[name] = updated
|
|
643
809
|
changed = True
|
coretrace_python/plugins/api.py
CHANGED
|
@@ -33,7 +33,7 @@ from coretrace_python.interprocedural import (
|
|
|
33
33
|
)
|
|
34
34
|
from coretrace_python.ir.lowering import analyzable_functions
|
|
35
35
|
from coretrace_python.semantic.imports import ImportAnalysis, ImportTable
|
|
36
|
-
from coretrace_python.taint import EntryPointAnalysis, Model
|
|
36
|
+
from coretrace_python.taint import EntryPointAnalysis, Model, ProjectTemplates
|
|
37
37
|
|
|
38
38
|
PLUGIN_API_VERSION = 1
|
|
39
39
|
|
|
@@ -69,9 +69,10 @@ class ModelPlugin(Plugin):
|
|
|
69
69
|
|
|
70
70
|
class ProjectContext:
|
|
71
71
|
"""What a project-scoped plugin sees: the module graph, the dependency graph, the
|
|
72
|
-
advisories every plugin and advisory file contributed, the dependency policy,
|
|
73
|
-
|
|
74
|
-
and functions of modules it
|
|
72
|
+
advisories every plugin and advisory file contributed, the dependency policy, each
|
|
73
|
+
module's imports, call graph and functions, and the project's templates with the
|
|
74
|
+
filters they call. The engine passes the call graphs and functions of modules it
|
|
75
|
+
served from its cache."""
|
|
75
76
|
|
|
76
77
|
def __init__(
|
|
77
78
|
self,
|
|
@@ -83,8 +84,10 @@ class ProjectContext:
|
|
|
83
84
|
policy: Policy | None = None,
|
|
84
85
|
root: Path | None = None,
|
|
85
86
|
functions: Mapping[str, tuple[ModuleFunction, ...]] | None = None,
|
|
87
|
+
templates: ProjectTemplates | None = None,
|
|
86
88
|
) -> None:
|
|
87
89
|
self.root = root
|
|
90
|
+
self.templates = templates or ProjectTemplates()
|
|
88
91
|
self.graph = graph
|
|
89
92
|
self.dependencies = dependencies
|
|
90
93
|
self.advisories = advisories
|
|
@@ -32,19 +32,31 @@ from coretrace_python.taint.models import (
|
|
|
32
32
|
Validator,
|
|
33
33
|
)
|
|
34
34
|
from coretrace_python.taint.routes import RegisteredRoutes, Routes, registered_routes
|
|
35
|
-
from coretrace_python.taint.templates import
|
|
35
|
+
from coretrace_python.taint.templates import (
|
|
36
|
+
FILTER_FUNCTIONS,
|
|
37
|
+
EscapedTemplates,
|
|
38
|
+
FilterCall,
|
|
39
|
+
ProjectTemplates,
|
|
40
|
+
escaped_templates,
|
|
41
|
+
project_templates,
|
|
42
|
+
request_processor,
|
|
43
|
+
unread_renders,
|
|
44
|
+
)
|
|
36
45
|
|
|
37
46
|
__all__ = [
|
|
47
|
+
"FILTER_FUNCTIONS",
|
|
38
48
|
"TEXT_KINDS",
|
|
39
49
|
"AuthorizationGuard",
|
|
40
50
|
"EntryPoint",
|
|
41
51
|
"EntryPointAnalysis",
|
|
42
52
|
"EscapedTemplates",
|
|
53
|
+
"FilterCall",
|
|
43
54
|
"Members",
|
|
44
55
|
"Model",
|
|
45
56
|
"ModelError",
|
|
46
57
|
"ModelTable",
|
|
47
58
|
"NamedParameter",
|
|
59
|
+
"ProjectTemplates",
|
|
48
60
|
"RegisteredRoutes",
|
|
49
61
|
"RequestObject",
|
|
50
62
|
"RouteRegistrar",
|
|
@@ -65,6 +77,9 @@ __all__ = [
|
|
|
65
77
|
"TypedParameter",
|
|
66
78
|
"Validator",
|
|
67
79
|
"escaped_templates",
|
|
80
|
+
"project_templates",
|
|
68
81
|
"propagate_taint",
|
|
69
82
|
"registered_routes",
|
|
83
|
+
"request_processor",
|
|
84
|
+
"unread_renders",
|
|
70
85
|
]
|