coretrace-python-analyzer 0.13.0__py3-none-any.whl → 0.15.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.
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.13.0"
3
+ __version__ = "0.15.0"
4
4
 
@@ -0,0 +1,98 @@
1
+ """The constant text a string starts with (architecture §18): what ``+`` and f-strings
2
+ build from string literals and from module-level names bound once to one. Anything
3
+ else, a parameter, a call result, an import, ``%`` or ``format``, is text of unknown
4
+ value."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from types import MappingProxyType
10
+ from typing import ClassVar
11
+
12
+ from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
13
+ from coretrace_python.cfg import CFGError
14
+ from coretrace_python.hir import nodes
15
+ from coretrace_python.ir.lowering import (
16
+ MODULE_BODY,
17
+ LoweringError,
18
+ PyIRAnalysis,
19
+ analyzable_functions,
20
+ )
21
+ from coretrace_python.ir.model import (
22
+ BinaryOp,
23
+ BuildString,
24
+ Constant,
25
+ ForNext,
26
+ Global,
27
+ Instruction,
28
+ SetGlobal,
29
+ StoreLocal,
30
+ Value,
31
+ )
32
+
33
+
34
+ def leading_text(value: Value, defs: Mapping[Value, Instruction], strings: Mapping[str, str]) -> tuple[str, bool]:
35
+ """The constant text ``value`` starts with, and whether that text is all of it, given
36
+ the definitions of the function's values and the module's ``strings``."""
37
+
38
+ made = defs.get(value)
39
+ if isinstance(made, Constant):
40
+ return (made.value, True) if isinstance(made.value, str) else ("", False)
41
+ if isinstance(made, Global):
42
+ known = strings.get(made.name)
43
+ return ("", False) if known is None else (known, True)
44
+ if isinstance(made, BinaryOp) and made.operator == "add":
45
+ left, whole = leading_text(made.left, defs, strings)
46
+ if not whole:
47
+ return left, False
48
+ right, whole = leading_text(made.right, defs, strings)
49
+ return left + right, whole
50
+ if isinstance(made, BuildString):
51
+ text = ""
52
+ for part in made.parts:
53
+ piece, whole = leading_text(part, defs, strings)
54
+ text += piece
55
+ if not whole:
56
+ return text, False
57
+ return text, True
58
+ return "", False
59
+
60
+
61
+ class ModuleStringsAnalysis(Analysis[Mapping[str, str]]):
62
+ """The module-level names bound once, to a string literal, and never rebound: by no
63
+ other statement of the module body, no function assigning them as ``global``, no
64
+ definition of that name. A module that does not lower entirely has none."""
65
+
66
+ name: ClassVar[str] = "abstract.module_strings"
67
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
68
+
69
+ @classmethod
70
+ def compute(cls, ctx: AnalysisContext) -> Mapping[str, str]:
71
+ try:
72
+ functions = [ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module)]
73
+ except (LoweringError, CFGError):
74
+ return MappingProxyType({})
75
+ body = next((function for function in functions if function.name == MODULE_BODY), None)
76
+ if body is None:
77
+ return MappingProxyType({})
78
+ defs = {i.result: i for block in body.blocks for i in block.instructions if i.result is not None}
79
+ stored: dict[str, list[Value | None]] = {}
80
+ for block in body.blocks:
81
+ for instruction in block.instructions:
82
+ if isinstance(instruction, StoreLocal):
83
+ stored.setdefault(instruction.name, []).append(instruction.value)
84
+ if isinstance(block.terminator, ForNext):
85
+ # A ``for`` binds its target to each item.
86
+ stored.setdefault(block.terminator.target, []).append(None)
87
+ rebound = {
88
+ i.name for function in functions for block in function.blocks for i in block.instructions
89
+ if isinstance(i, SetGlobal)
90
+ }
91
+ rebound |= {s.name for s in ctx.module.body if isinstance(s, nodes.Function | nodes.Class)}
92
+ found: dict[str, str] = {}
93
+ for name, values in stored.items():
94
+ value = values[0] if len(values) == 1 and name not in rebound else None
95
+ made = defs.get(value) if value is not None else None
96
+ if isinstance(made, Constant) and isinstance(made.value, str):
97
+ found[name] = made.value
98
+ return MappingProxyType(found)
@@ -1,12 +1,26 @@
1
- """Open redirect: attacker-controlled input reaching a open redirect sink."""
1
+ """Open redirect: attacker-controlled input reaching a open redirect sink.
2
+
3
+ The rule draws its verdict from what the target's constant text proves (``taint.urls``),
4
+ as a browser resolves it. A reference on the current site (``/profile/``, ``?page=``,
5
+ ``profile/``) keeps the browser on the site, where any further redirect is the project's
6
+ own code, analysed on its own: refuted. A fixed absolute host sends the browser there,
7
+ but the input chooses the path, where that host may redirect again: a hotspot. Anything
8
+ unproven, ``"/" + next`` included, stays a vulnerability.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
5
13
  from typing import ClassVar
6
14
 
15
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
+ from coretrace_python.analysis import AnyAnalysis
7
17
  from coretrace_python.findings import Severity
8
- from coretrace_python.plugins import TaintDetector
9
- from coretrace_python.taint import TaintKind
18
+ from coretrace_python.findings.refutation import Status, Verdict
19
+ from coretrace_python.hir import nodes
20
+ from coretrace_python.ir.ssa import SSAAnalysis
21
+ from coretrace_python.plugins import PluginContext, TaintDetector
22
+ from coretrace_python.taint import TaintFlow, TaintKind
23
+ from coretrace_python.taint.urls import flow_url
10
24
 
11
25
 
12
26
  class OpenRedirectPlugin(TaintDetector):
@@ -15,3 +29,19 @@ class OpenRedirectPlugin(TaintDetector):
15
29
  kind: ClassVar[TaintKind] = TaintKind.REDIRECT
16
30
  severity: ClassVar[Severity] = Severity.MEDIUM
17
31
  title: ClassVar[str] = "Open redirect"
32
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
33
+
34
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
35
+ ssa = ctx.get(SSAAnalysis, function)
36
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
37
+ target = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
38
+ if target.relative:
39
+ return Verdict(flow, Status.REFUTED, f"the browser stays on the site: the target starts with {target.text!r}")
40
+ if target.origin is not None and verdict.status is Status.VULNERABILITY:
41
+ return Verdict(
42
+ flow,
43
+ Status.HOTSPOT,
44
+ f"the browser goes to the host fixed by {target.origin!r}, but the input chooses the path, "
45
+ "where that host may redirect again",
46
+ )
47
+ return verdict
@@ -1,7 +1,7 @@
1
1
  name = "open-redirect"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.open-redirect"]
6
6
 
7
7
  [entrypoint]
@@ -1,7 +1,7 @@
1
1
  name = "ssrf"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.ssrf"]
6
6
 
7
7
  [entrypoint]
@@ -1,12 +1,26 @@
1
- """Server-side request forgery: attacker-controlled input reaching a SSRF sink."""
1
+ """Server-side request forgery: attacker-controlled input reaching a SSRF sink.
2
+
3
+ The rule draws its verdict from what the URL's constant text proves (``taint.urls``).
4
+ A fixed host keeps the input from choosing it, but the input still chooses the path,
5
+ which may reach a sensitive resource of that host, and a redirect may lead elsewhere: a
6
+ hotspot. With the path fixed too, the input reaching the query or fragment only, and
7
+ redirects disabled in the call, the destination is fixed: refuted. Anything unproven,
8
+ a base of unknown value or a URL built in a helper function, stays a vulnerability.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
5
13
  from typing import ClassVar
6
14
 
15
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
16
+ from coretrace_python.analysis import AnyAnalysis
7
17
  from coretrace_python.findings import Severity
8
- from coretrace_python.plugins import TaintDetector
9
- from coretrace_python.taint import TaintKind
18
+ from coretrace_python.findings.refutation import Status, Verdict
19
+ from coretrace_python.hir import nodes
20
+ from coretrace_python.ir.ssa import SSAAnalysis
21
+ from coretrace_python.plugins import PluginContext, TaintDetector
22
+ from coretrace_python.taint import TaintFlow, TaintKind
23
+ from coretrace_python.taint.urls import flow_url, redirects_disabled
10
24
 
11
25
 
12
26
  class SsrfPlugin(TaintDetector):
@@ -15,3 +29,21 @@ class SsrfPlugin(TaintDetector):
15
29
  kind: ClassVar[TaintKind] = TaintKind.SSRF
16
30
  severity: ClassVar[Severity] = Severity.HIGH
17
31
  title: ClassVar[str] = "Server-side request forgery"
32
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
33
+
34
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
35
+ ssa = ctx.get(SSAAnalysis, function)
36
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
37
+ url = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
38
+ if url.origin is None:
39
+ return verdict
40
+ no_redirects = redirects_disabled(flow.sink_arguments)
41
+ if url.path_fixed:
42
+ if no_redirects:
43
+ return Verdict(flow, Status.REFUTED, f"the destination is fixed by {url.text!r} and redirects are disabled")
44
+ left = "a redirect may lead elsewhere"
45
+ else:
46
+ left = "the input chooses the path" + ("" if no_redirects else ", and a redirect may lead elsewhere")
47
+ if verdict.status is not Status.VULNERABILITY:
48
+ return verdict
49
+ return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {url.origin!r}, but {left}")
@@ -251,7 +251,7 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
251
251
  default = entry.get("default", False)
252
252
  if not isinstance(default, bool):
253
253
  raise TypeError(f"condition default must be true or false, got {default!r}")
254
- return Condition(
254
+ condition = Condition(
255
255
  str(entry["kind"]),
256
256
  str(entry["text"]),
257
257
  None if entry.get("argument") is None else str(entry["argument"]),
@@ -259,3 +259,8 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
259
259
  position,
260
260
  default,
261
261
  )
262
+ if condition.kind == "host" and (
263
+ (condition.argument is None and condition.position is None) or condition.values or condition.default
264
+ ):
265
+ raise ValueError("a host condition names the URL argument, by keyword or position, and nothing else")
266
+ return condition
@@ -10,7 +10,7 @@ are correlated here into one high-confidence ``exploitable-vulnerability`` findi
10
10
  from __future__ import annotations
11
11
 
12
12
  from collections.abc import Iterable, Mapping
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, replace
14
14
 
15
15
  from coretrace_python.dependency.graph import (
16
16
  DIRECT,
@@ -24,6 +24,7 @@ from coretrace_python.findings.refutation import Status, Verdict, Verdicts
24
24
  from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol
25
25
  from coretrace_python.semantic.symbols import SymbolId
26
26
  from coretrace_python.taint import Sink, TaintFlow, TaintKind
27
+ from coretrace_python.taint.urls import UrlProof, redirects_disabled
27
28
 
28
29
  Affected = Mapping[SymbolId, tuple[Advisory, ...]]
29
30
 
@@ -61,14 +62,33 @@ class ConditionCheck:
61
62
  passed: str | None = None
62
63
 
63
64
 
64
- def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | None) -> ConditionCheck:
65
- """Decide ``entry``'s conditions against what the call's ``arguments`` denote."""
65
+ def check_conditions(
66
+ entry: AdvisoryEntryPoint | None,
67
+ arguments: Arguments | None,
68
+ url: UrlProof | None = None,
69
+ passed_as: frozenset[tuple[int | None, str | None]] = frozenset(),
70
+ ) -> ConditionCheck:
71
+ """Decide ``entry``'s conditions against what the call's ``arguments`` denote and, for
72
+ a ``host`` condition, what ``url`` proves of the value the attacker's input is passed
73
+ in (``passed_as``). A host the URL's constant text fixes rules the call out when the
74
+ call disables redirects; otherwise a redirect may still lead to a host the attacker
75
+ controls, and the condition stays pending with that uncertainty."""
66
76
 
67
77
  if entry is None:
68
78
  return ConditionCheck()
69
79
  met: list[Condition] = []
70
80
  pending: list[Condition] = []
71
81
  for condition in entry.conditions:
82
+ if condition.kind == "host":
83
+ origin = url.origin if url is not None and _passed_in(condition, passed_as) else None
84
+ if origin is None:
85
+ pending.append(condition)
86
+ elif redirects_disabled(arguments):
87
+ return ConditionCheck(tuple(met), tuple(pending), condition, repr(origin))
88
+ else:
89
+ text = f"the host is fixed by {origin!r}, but a redirect may lead to a host the attacker controls"
90
+ pending.append(replace(condition, text=text))
91
+ continue
72
92
  given = arguments.given(condition.argument, condition.position) if condition.checkable and arguments is not None else None
73
93
  if given is None:
74
94
  pending.append(condition)
@@ -84,6 +104,16 @@ def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | No
84
104
  return ConditionCheck(tuple(met), tuple(pending))
85
105
 
86
106
 
107
+ def _passed_in(condition: Condition, passed_as: frozenset[tuple[int | None, str | None]]) -> bool:
108
+ """Whether a value passed as ``passed_as`` is the argument ``condition`` names."""
109
+
110
+ return any(
111
+ (keyword is not None and keyword == condition.argument)
112
+ or (keyword is None and position is not None and position == condition.position)
113
+ for position, keyword in passed_as
114
+ )
115
+
116
+
87
117
  def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
88
118
  """Which call a contradicted condition rules out, and why: ``app:12
89
119
  python.yaml.load(Loader=python.yaml.SafeLoader)``."""
@@ -120,9 +150,10 @@ def correlate(
120
150
  flows: Iterable[TaintFlow],
121
151
  verdicts: Verdicts | None,
122
152
  affected: Affected,
153
+ urls: Mapping[TaintFlow, UrlProof] | None = None,
123
154
  ) -> tuple[Finding, ...]:
124
155
  """Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function,
125
- one per advisory the sink is affected by."""
156
+ one per advisory the sink is affected by; ``urls`` holds what each flow's URL proves."""
126
157
 
127
158
  findings: list[Finding] = []
128
159
  for flow in flows:
@@ -136,7 +167,7 @@ def correlate(
136
167
  entry = advisory.entry_point(flow.sink.symbol)
137
168
  if entry is not None and not any(entry.exploitable_through(p, k) for p, k in flow.passed_as):
138
169
  continue
139
- check = check_conditions(entry, flow.sink_arguments)
170
+ check = check_conditions(entry, flow.sink_arguments, (urls or {}).get(flow), flow.passed_as)
140
171
  if check.contradicted is None:
141
172
  findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check))
142
173
  return tuple(findings)
@@ -151,8 +151,10 @@ class Condition:
151
151
  ``argument`` condition names the argument by keyword, and by ``position`` when it may
152
152
  be passed positionally, and lists the ``values`` that satisfy it — symbols
153
153
  (``python.yaml.FullLoader``) or constants as Python writes them (``True``);
154
- ``default`` says an absent argument means a vulnerable value. A ``semantic``
155
- condition cannot be checked and is reported as pending review."""
154
+ ``default`` says an absent argument means a vulnerable value. A ``host`` condition
155
+ says the attacker must choose the host of the URL passed as ``argument`` (or at
156
+ ``position``); the engine decides it from what the URL's constant text proves. A
157
+ ``semantic`` condition cannot be checked and is reported as pending review."""
156
158
 
157
159
  kind: str
158
160
  text: str
@@ -17,6 +17,7 @@ from typing import Any, ClassVar
17
17
 
18
18
  from coretrace_python import __version__
19
19
  from coretrace_python.abstract import ConstantPropagation, HeapAnalysis, RangeAnalysis
20
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis
20
21
  from coretrace_python.analysis import (
21
22
  AnalysisContext,
22
23
  AnalysisManager,
@@ -125,6 +126,7 @@ from coretrace_python.taint import (
125
126
  escaped_templates,
126
127
  registered_routes,
127
128
  )
129
+ from coretrace_python.taint.urls import flow_url
128
130
 
129
131
  TOOL_NAME = "coretrace-python-analyzer"
130
132
 
@@ -143,6 +145,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
143
145
  DefUseAnalysis,
144
146
  ConstantPropagation,
145
147
  RangeAnalysis,
148
+ ModuleStringsAnalysis,
146
149
  HeapAnalysis,
147
150
  CallGraphAnalysis,
148
151
  SummaryAnalysis,
@@ -768,14 +771,14 @@ def _analyse_module(
768
771
  graph = manager.get(CallGraphAnalysis)
769
772
  correlated: list[Finding] = []
770
773
  if affected:
774
+ strings = manager.get(ModuleStringsAnalysis)
771
775
  for function in supported:
776
+ flows = manager.get(TaintAnalysis, function).flows
777
+ ssa = manager.get(SSAAnalysis, function)
778
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
779
+ urls = {flow: flow_url(flow, defs, strings) for flow in flows if flow.kinds & TaintKind.ADVISORY}
772
780
  correlated.extend(
773
- correlate(
774
- graph.name_of(function),
775
- manager.get(TaintAnalysis, function).flows,
776
- manager.get(RefutationAnalysis, function),
777
- affected,
778
- )
781
+ correlate(graph.name_of(function), flows, manager.get(RefutationAnalysis, function), affected, urls)
779
782
  )
780
783
  return CachedModule(
781
784
  manager.get(EntryPointAnalysis),
@@ -13,13 +13,14 @@ from typing import ClassVar
13
13
 
14
14
  from coretrace_python.analysis import AnyAnalysis
15
15
  from coretrace_python.findings import Confidence, Finding, Severity
16
- from coretrace_python.findings.refutation import RefutationAnalysis, Status
16
+ from coretrace_python.findings.refutation import RefutationAnalysis, Status, Verdict
17
+ from coretrace_python.hir import nodes
17
18
  from coretrace_python.interprocedural import CallGraphAnalysis
18
19
  from coretrace_python.ir.model import Call, Instruction, Symbol, Value
19
20
  from coretrace_python.ir.ssa import SSAAnalysis
20
21
  from coretrace_python.plugins.api import Plugin, PluginContext
21
22
  from coretrace_python.semantic.symbols import SymbolId
22
- from coretrace_python.taint import TaintAnalysis, TaintKind
23
+ from coretrace_python.taint import TaintAnalysis, TaintFlow, TaintKind
23
24
 
24
25
 
25
26
  class TaintDetector(Plugin):
@@ -40,6 +41,9 @@ class TaintDetector(Plugin):
40
41
  if not flow.kinds & self.kind:
41
42
  continue
42
43
  verdict = verdicts.verdict(flow)
44
+ if verdict.status is Status.REFUTED:
45
+ continue
46
+ verdict = self.judge(ctx, function, flow, verdict)
43
47
  if verdict.status is Status.REFUTED:
44
48
  continue
45
49
  message = f"{self.title}: {flow.source.label} input reaches {flow.sink.symbol}"
@@ -70,6 +74,13 @@ class TaintDetector(Plugin):
70
74
  )
71
75
  return findings
72
76
 
77
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
78
+ """This rule's verdict on ``flow``, given the refutation's: the same, unless the rule
79
+ knows more about its own sinks. The refutation's verdict, which other consumers
80
+ of the flow read, does not change."""
81
+
82
+ return verdict
83
+
73
84
 
74
85
  def _lower(confidence: Confidence) -> Confidence:
75
86
  return {Confidence.HIGH: Confidence.MEDIUM}.get(confidence, Confidence.LOW)
@@ -0,0 +1,70 @@
1
+ """What the constant text a URL starts with proves about its destination, and whether a
2
+ call follows redirects: one proof that the rules reading URLs share, each drawing its
3
+ own verdict from it. The text is what ``+`` and f-strings build before any other value
4
+ (``abstract.strings``); the proof says nothing about the rest of the URL."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+
12
+ from coretrace_python.abstract.strings import leading_text
13
+ from coretrace_python.interprocedural import Arguments
14
+ from coretrace_python.ir.model import Instruction, Value
15
+ from coretrace_python.taint.engine import TaintFlow
16
+
17
+ # ``scheme://`` and an authority ended by ``/``, ``?`` or ``#``. A backslash, which some
18
+ # URL parsers take for a slash, does not end it here.
19
+ _ORIGIN = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+[/?#]")
20
+ # The same with the path fixed too: what follows is query or fragment.
21
+ _PATH = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+(?:/[^?#]*)?[?#]")
22
+ # A reference a browser resolves on the current site: ``/`` then a character that is not
23
+ # a slash, a backslash, a space or a control character (``//`` and ``/\`` name another
24
+ # host, and a browser drops tabs and newlines), a query or a fragment, or a relative path
25
+ # whose first segment ends in the text without a ``:`` that would make it a scheme.
26
+ _RELATIVE = re.compile(r"/[^/\\\x00-\x20\x7f]|[?#]|[^/\\?#:\x00-\x20\x7f][^/\\?#:]*[/?#]")
27
+ # A client honours the one of these keywords it knows and rejects the other with a
28
+ # ``TypeError``: either way, the call follows no redirect.
29
+ _NO_REDIRECTS = ("allow_redirects", "follow_redirects")
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class UrlProof:
34
+ """What the constant ``text`` a URL starts with proves: the ``origin`` it fixes
35
+ (``scheme://authority`` and the character ending it), whether it fixes the path
36
+ too (``path_fixed``), and whether it starts a reference a browser resolves on the
37
+ current site (``relative``)."""
38
+
39
+ text: str
40
+ origin: str | None = None
41
+ path_fixed: bool = False
42
+ relative: bool = False
43
+
44
+
45
+ def url_proof(text: str) -> UrlProof:
46
+ origin = _ORIGIN.match(text)
47
+ return UrlProof(
48
+ text,
49
+ origin.group(0) if origin is not None else None,
50
+ _PATH.match(text) is not None,
51
+ _RELATIVE.match(text) is not None,
52
+ )
53
+
54
+
55
+ def flow_url(flow: TaintFlow, defs: Mapping[Value, Instruction], strings: Mapping[str, str]) -> UrlProof:
56
+ """The proof for the value ``flow`` passes to its sink, ``defs`` defining the values
57
+ of the function making the call. A flow reaching its sink in a callee proves nothing:
58
+ the value is built there."""
59
+
60
+ if flow.through is not None:
61
+ return UrlProof("")
62
+ text, _ = leading_text(flow.argument, defs, strings)
63
+ return url_proof(text)
64
+
65
+
66
+ def redirects_disabled(arguments: Arguments | None) -> bool:
67
+ """Whether the call disables redirects explicitly (``allow_redirects=False`` or
68
+ ``follow_redirects=False``); absent, a variable or unpacked options do not."""
69
+
70
+ return arguments is not None and any(arguments.given(name) == (True, "False") for name in _NO_REDIRECTS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.13.0
3
+ Version: 0.15.0
4
4
  Summary: Static security analysis infrastructure for Python code
5
5
  Project-URL: Homepage, https://github.com/CoreTrace/coretrace-python-analyzer
6
6
  Project-URL: Repository, https://github.com/CoreTrace/coretrace-python-analyzer
@@ -1,12 +1,13 @@
1
- coretrace_python/__init__.py,sha256=4_uvCC4RHPELcdUoMgQ00MwplbVOBvuK0TpVzxBnXII,76
1
+ coretrace_python/__init__.py,sha256=LF0lqv_rMHWvWLyAat8Ysu4q_nrKXQ35Q-QHFk2BarI,76
2
2
  coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
3
3
  coretrace_python/cache.py,sha256=-pZv-ELm4jMCTXQWSZIobHaT9UHIjFu7IlMkOM0i4LI,13607
4
4
  coretrace_python/cli.py,sha256=G5jhb6g6RgSRSjjS7DBgziIjUM1vTFwalJGvFHVKxrg,11225
5
- coretrace_python/engine.py,sha256=nwK8fbcEOT0zKiBlK67aCl7yiQpE00j56LLknXn7Djg,32276
5
+ coretrace_python/engine.py,sha256=Px9_zXYfBUbSEitgWcseEhza-h4j6hL_qFE8kuogrMY,32678
6
6
  coretrace_python/abstract/__init__.py,sha256=n5FNkV8Z4LmwG0r06KWdLj4N2CPSgAKwY3896tdmRgM,999
7
7
  coretrace_python/abstract/constants.py,sha256=z4-e7IzysxzGi58wFFff1MlEoV8l7AZsB9EL-q6b7vY,9071
8
8
  coretrace_python/abstract/heap.py,sha256=OaESXFGC0qzyhAr76xrCu4skeDVzZM4wlenQqfMTkdk,9642
9
9
  coretrace_python/abstract/ranges.py,sha256=zEl9ee55u3E02tkYzzIvCCud5mYRKIUgZd9h_0-crCM,11547
10
+ coretrace_python/abstract/strings.py,sha256=G5EGlhxjrpjU9TuXQifuVO-6MNMrUtZ_4S3nkMEvIuQ,4095
10
11
  coretrace_python/abstract/values.py,sha256=SQmgR8jo7cd8KJFDokejg-cFTjJU1CTBUPYyEvbMg-4,1739
11
12
  coretrace_python/analysis/__init__.py,sha256=hE7lDwXOdA05_aSZaERRgMv0C2ev8jjJbz4xvrja4Og,728
12
13
  coretrace_python/analysis/manager.py,sha256=suoIrPwimyu1AjF6BJwpqAkJBwe6XxzQu9JwV2TN2aQ,5843
@@ -55,16 +56,16 @@ coretrace_python/bundled/security/insecure_deserialization/insecure_deserializat
55
56
  coretrace_python/bundled/security/insecure_deserialization/plugin.toml,sha256=5OTVO9jZN-HBid8v__jYkxU-RnKX974Qqu7JQMFdj54,268
56
57
  coretrace_python/bundled/security/nosql_injection/nosql_injection.py,sha256=0MCTZ3DjOf5DQrLgnPPgpa80CYUd5teQ_iau3s_82O0,789
57
58
  coretrace_python/bundled/security/nosql_injection/plugin.toml,sha256=FP2qTPtH7ZCiu6vwfgadWlXfVnlKOigXJ43cv9PlorA,232
58
- coretrace_python/bundled/security/open_redirect/open_redirect.py,sha256=nUjVIgYhVh_2xBs8_1LOfcLDgQ3g3z1E7APqg2sBdpo,562
59
- coretrace_python/bundled/security/open_redirect/plugin.toml,sha256=8t4KLNG1DIpjjen6TuKxMjz2jv6GDcwVJDm_hJUalCg,224
59
+ coretrace_python/bundled/security/open_redirect/open_redirect.py,sha256=t2LiqdgoD8A_ZG0iG5kZ88ZnSEcnngpHNbZUzCM92Ek,2365
60
+ coretrace_python/bundled/security/open_redirect/plugin.toml,sha256=2SMFY7GeczfUqeX4fp600Qj24k8fUPCg4zD__JBabkw,261
60
61
  coretrace_python/bundled/security/path_traversal/path_traversal.py,sha256=MiTDZBNU6i3WWua1qaxg1swqDy3wlWwBwG6G4ClyDgw,552
61
62
  coretrace_python/bundled/security/path_traversal/plugin.toml,sha256=f0ADZimUvQatOffltF5Ihu1mkzf5rNrsaIpKBg2v39c,228
62
63
  coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py,sha256=QaE2CXAdl0bi4dE5d2d8KnLJrsAcxUDjR76ghfsnXXo,900
63
64
  coretrace_python/bundled/security/plaintext_credentials/plugin.toml,sha256=W56wUdAdf7NNirfQhTdKrn-cCwebqmvkRJg9OVFJUyM,263
64
65
  coretrace_python/bundled/security/sql_injection/plugin.toml,sha256=U35h6YdbbM93Ar_msWrwY98_bb6rN-qaa1e7XOtPeSo,224
65
66
  coretrace_python/bundled/security/sql_injection/sql_injection.py,sha256=W-ppZngb9mLwRjJPGkh__8kaDqYdSrVbciv0mPdYFNE,545
66
- coretrace_python/bundled/security/ssrf/plugin.toml,sha256=OySYgL9rGh1f4UuVpEOWdgcyp0vAmaP1m2IJ8Al6Ddc,189
67
- coretrace_python/bundled/security/ssrf/ssrf.py,sha256=bshcPZMjuQtTR4203CUTxFGZuwIYfsi58rnE5XdqKXc,549
67
+ coretrace_python/bundled/security/ssrf/plugin.toml,sha256=Uh0hvcc2bkOEKBQUw6tyuvpPQRq8_JzhWc9nVQAdm1Y,226
68
+ coretrace_python/bundled/security/ssrf/ssrf.py,sha256=OBe7GcYvdXLpFHsdt7wPA9-UPN6i1tBiDx6a79SL6yg,2532
68
69
  coretrace_python/bundled/security/xss/plugin.toml,sha256=o6aZWoDNCG6CJ11JDa5lVR0v6NHh-Jg5QpNRxoGSJaw,185
69
70
  coretrace_python/bundled/security/xss/xss.py,sha256=hzhoblPMivtwoWZ4v7YyAb15GGisX_imGWgxFzCDeqA,532
70
71
  coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py,sha256=_ktrigLDf-XTKjWtYX-_wZ9-Gb3G4bCzRhpU0Xj-c1g,723
@@ -93,9 +94,9 @@ coretrace_python/dataflow/__init__.py,sha256=N5uY8VTNjEk6-ARCgDkxr6uGqQIz2NbF-U9
93
94
  coretrace_python/dataflow/lattice.py,sha256=HgYGVcKVUlexvEc0yT47aQCudLxU_SWLpHhr17igkiE,1929
94
95
  coretrace_python/dataflow/solver.py,sha256=iDsW0wujhM2DXplu2JMKC46BRsroXlNETHPEoikPeG4,3475
95
96
  coretrace_python/dependency/__init__.py,sha256=BxkxnNwIEpAYtwbshCQovG-YXJ80QbwyzkmyJdbgsVg,1184
96
- coretrace_python/dependency/advisories.py,sha256=zti7MrojcfxfYWJuGpyLXXJU3pffpT4SJnQUa_g34Lw,10274
97
- coretrace_python/dependency/correlation.py,sha256=GLtrMn5yYr4NcKYNYZTqIFpwoXOrgsqI9t6lggXxFbg,7388
98
- coretrace_python/dependency/graph.py,sha256=HtvKeWWG9cy9TzJQ7lYMGPfHjPBxoSVgwzoj71SegaE,18445
97
+ coretrace_python/dependency/advisories.py,sha256=RPtqT5M_kmRVnjs53PRnDJKUsUysdJmk8KuE3sigUOk,10564
98
+ coretrace_python/dependency/correlation.py,sha256=nr5FZ3yOT4HsKzvEAFlzzaPJxBc9_X-IuyOWtRjS67Q,9033
99
+ coretrace_python/dependency/graph.py,sha256=U9KzdVCcM7P1W6qSSSMkzMIIVEh3qlzBpBi2FAFy0y0,18633
99
100
  coretrace_python/dependency/policy.py,sha256=bsxZXL9xC0jnAzkzDh6ncnW8oKLG2H13jYmXF_q99Ng,2454
100
101
  coretrace_python/dependency/sbom.py,sha256=rzJ-t0Mwoghej8nk0N63Dpjr_wYXm6JzuOPXjD8Kqs4,3234
101
102
  coretrace_python/dependency/vex.py,sha256=YddBuXsv7UgEnHdM9Jfjpo_A700IuIqRMjQz7AHSLX4,7437
@@ -124,7 +125,7 @@ coretrace_python/ir/printer.py,sha256=otBIhZUvB_UfB68gid_FbPcSjBUSmxH3v7vBMZZQVk
124
125
  coretrace_python/ir/ssa.py,sha256=cIYbcv90PI6AOeMrw2ZWp88fy7E2ZO4P3tqjtPHra7g,11116
125
126
  coretrace_python/plugins/__init__.py,sha256=xRKqkvkvQMmfQACHHoBioYhIyyK4HoMAVogy6EZG8tg,1398
126
127
  coretrace_python/plugins/api.py,sha256=FcIe2c4sFB-PAPtYcJkidvdZJkr_xFKT8SvlBP5l38o,8603
127
- coretrace_python/plugins/detectors.py,sha256=ej5Z_MS0CBqrR4b9xycjTVUIo5arpArpdbr7nL732iI,5700
128
+ coretrace_python/plugins/detectors.py,sha256=9c6yaLvf5nwtiiIskLd9WE_R4IE_RcqjOD5c-SH0SuI,6274
128
129
  coretrace_python/plugins/loader.py,sha256=Use-lHTL4-IJYQBZmODC89zhJbYKGvDSLZHWjZa5NaI,3885
129
130
  coretrace_python/plugins/manifest.py,sha256=F24pgQSwApRUL6ZihRSLv9wL_M03A_pZjrj9Zi4Ku3Q,3438
130
131
  coretrace_python/plugins/registry.py,sha256=ixKcT2YDky96TWCFckdzKnGRmFn1dbBVMSZLPvdxvY8,1177
@@ -147,9 +148,10 @@ coretrace_python/taint/engine.py,sha256=rEAbwjR5v294PqBzBlu6f9WiMvVmwV83FqdsWbT4
147
148
  coretrace_python/taint/models.py,sha256=8AwPKKdmwAHmE7CxwUbtWn5w7JVkEJ06_Is7xQWTx38,17066
148
149
  coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
149
150
  coretrace_python/taint/templates.py,sha256=8Ojb43VXuWxOVifeXRpi3_2mjZfnvIC7GQ-AW80GUYk,11148
150
- coretrace_python_analyzer-0.13.0.dist-info/METADATA,sha256=XNQT5nbo6DGzDqYJw9p1sYyciIkdituTpM82-o3z8-8,3217
151
- coretrace_python_analyzer-0.13.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
152
- coretrace_python_analyzer-0.13.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
153
- coretrace_python_analyzer-0.13.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
154
- coretrace_python_analyzer-0.13.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
155
- coretrace_python_analyzer-0.13.0.dist-info/RECORD,,
151
+ coretrace_python/taint/urls.py,sha256=kb0-uOT12ww7R3Jzs_lPKg9lFs74JI-bpa9pXX105xM,3166
152
+ coretrace_python_analyzer-0.15.0.dist-info/METADATA,sha256=MrHb31ufFhrM7ievaxqYvuwvTlqnif8HAvvQkVsfO30,3217
153
+ coretrace_python_analyzer-0.15.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
154
+ coretrace_python_analyzer-0.15.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
155
+ coretrace_python_analyzer-0.15.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
156
+ coretrace_python_analyzer-0.15.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
157
+ coretrace_python_analyzer-0.15.0.dist-info/RECORD,,