coretrace-python-analyzer 0.13.0__py3-none-any.whl → 0.14.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.14.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,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,37 @@
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
+ How the URL is built, with ``+`` and f-strings, can prove part of its destination. Constant
4
+ text fixing ``scheme://host`` and ending the authority with ``/``, ``?`` or ``#`` before
5
+ any input keeps the input from choosing the host; the input still chooses the path, which
6
+ may reach a sensitive resource of that host, and a redirect may lead elsewhere, so the
7
+ flow is a hotspot. With the path fixed too, the input reaching the query or fragment
8
+ only, and redirects disabled in the call, the destination is fixed and the flow is
9
+ refuted. Anything unproven, a base of unknown value or a URL built in a helper function,
10
+ stays a vulnerability.
11
+ """
2
12
 
3
13
  from __future__ import annotations
4
14
 
15
+ import re
5
16
  from typing import ClassVar
6
17
 
18
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
19
+ from coretrace_python.analysis import AnyAnalysis
7
20
  from coretrace_python.findings import Severity
8
- from coretrace_python.plugins import TaintDetector
9
- from coretrace_python.taint import TaintKind
21
+ from coretrace_python.findings.refutation import Status, Verdict
22
+ from coretrace_python.hir import nodes
23
+ from coretrace_python.ir.ssa import SSAAnalysis
24
+ from coretrace_python.plugins import PluginContext, TaintDetector
25
+ from coretrace_python.taint import TaintFlow, TaintKind
26
+
27
+ # ``scheme://`` and an authority ended by ``/``, ``?`` or ``#``. A backslash, which some
28
+ # URL parsers take for a slash, does not end it here.
29
+ _FIXED_HOST = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+[/?#]")
30
+ # The same with the path fixed too: what follows is query or fragment.
31
+ _FIXED_PATH = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+(?:/[^?#]*)?[?#]")
32
+ # A client honours the one of these keywords it knows and rejects the other with a
33
+ # ``TypeError``: either way, the call follows no redirect.
34
+ _NO_REDIRECTS = ("allow_redirects", "follow_redirects")
10
35
 
11
36
 
12
37
  class SsrfPlugin(TaintDetector):
@@ -15,3 +40,25 @@ class SsrfPlugin(TaintDetector):
15
40
  kind: ClassVar[TaintKind] = TaintKind.SSRF
16
41
  severity: ClassVar[Severity] = Severity.HIGH
17
42
  title: ClassVar[str] = "Server-side request forgery"
43
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
44
+
45
+ def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
46
+ if flow.through is not None:
47
+ return verdict # the URL is built in the callee
48
+ ssa = ctx.get(SSAAnalysis, function)
49
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
50
+ text, _ = leading_text(flow.argument, defs, ctx.get(ModuleStringsAnalysis))
51
+ host = _FIXED_HOST.match(text)
52
+ if host is None:
53
+ return verdict
54
+ arguments = flow.sink_arguments
55
+ no_redirects = arguments is not None and any(arguments.given(name) == (True, "False") for name in _NO_REDIRECTS)
56
+ if _FIXED_PATH.match(text) is not None:
57
+ if no_redirects:
58
+ return Verdict(flow, Status.REFUTED, f"the destination is fixed by {text!r} and redirects are disabled")
59
+ left = "a redirect may lead elsewhere"
60
+ else:
61
+ left = "the input chooses the path" + ("" if no_redirects else ", and a redirect may lead elsewhere")
62
+ if verdict.status is not Status.VULNERABILITY:
63
+ return verdict
64
+ return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {host.group(0)!r}, but {left}")
@@ -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,
@@ -143,6 +144,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
143
144
  DefUseAnalysis,
144
145
  ConstantPropagation,
145
146
  RangeAnalysis,
147
+ ModuleStringsAnalysis,
146
148
  HeapAnalysis,
147
149
  CallGraphAnalysis,
148
150
  SummaryAnalysis,
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.13.0
3
+ Version: 0.14.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=kY0Q5RzkkixmFjJXYSXgYgHOkh327TcAyqMH9u0a_M0,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=yICn93LZyoWOdDoZEIqoWATuhPQhLPW8ANLxJPskDwM,32371
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
@@ -63,8 +64,8 @@ coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py
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=Zk3eCu4Gp9vnR4DarG_RAm9U7hNv2ESAn_oVUVUMHF8,3441
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
@@ -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,9 @@ 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_analyzer-0.14.0.dist-info/METADATA,sha256=I6joOK1gGJSOzcn3pJ9PYii-2Yb7Ak-PSK9SmQWz4vQ,3217
152
+ coretrace_python_analyzer-0.14.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
153
+ coretrace_python_analyzer-0.14.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
154
+ coretrace_python_analyzer-0.14.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
155
+ coretrace_python_analyzer-0.14.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
156
+ coretrace_python_analyzer-0.14.0.dist-info/RECORD,,