coretrace-python-analyzer 0.22.0__py3-none-any.whl → 0.24.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/abstract/module_constants.py +121 -0
- coretrace_python/abstract/strings.py +12 -42
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +12 -2
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +8 -2
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +8 -2
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +27 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +2 -1
- coretrace_python/bundled/security/code_injection/code_injection.py +93 -5
- coretrace_python/bundled/security/code_injection/plugin.toml +1 -1
- coretrace_python/bundled/security/command_injection/command_injection.py +7 -1
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +8 -1
- coretrace_python/bundled/security/nosql_injection/nosql_injection.py +8 -1
- coretrace_python/bundled/security/open_redirect/open_redirect.py +8 -1
- coretrace_python/bundled/security/path_traversal/path_traversal.py +8 -1
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +8 -1
- coretrace_python/bundled/security/sql_injection/sql_injection.py +8 -1
- coretrace_python/bundled/security/ssrf/ssrf.py +8 -1
- coretrace_python/bundled/security/xss/xss.py +8 -1
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +8 -1
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +8 -2
- coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +8 -1
- coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +7 -1
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +8 -2
- coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +7 -1
- coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +1 -1
- coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +98 -12
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +8 -1
- coretrace_python/bundled/syntax/weak_random/weak_random.py +8 -2
- coretrace_python/cache.py +21 -15
- coretrace_python/cli.py +36 -6
- coretrace_python/engine.py +384 -121
- coretrace_python/findings/__init__.py +10 -1
- coretrace_python/findings/context.py +57 -0
- coretrace_python/findings/model.py +11 -1
- coretrace_python/findings/refutation.py +210 -27
- coretrace_python/findings/rules.py +16 -0
- coretrace_python/frontend/parser.py +7 -1
- coretrace_python/interprocedural/__init__.py +4 -0
- coretrace_python/interprocedural/callgraph.py +14 -1
- coretrace_python/interprocedural/modulegraph.py +54 -28
- coretrace_python/interprocedural/summaries.py +140 -32
- coretrace_python/ir/defuse.py +26 -1
- coretrace_python/ir/lowering.py +2 -1
- coretrace_python/ir/model.py +5 -2
- coretrace_python/ir/printer.py +4 -1
- coretrace_python/plugins/__init__.py +7 -1
- coretrace_python/plugins/api.py +21 -4
- coretrace_python/plugins/detectors.py +27 -4
- coretrace_python/plugins/secrets.py +33 -13
- coretrace_python/reporters/report.py +6 -2
- coretrace_python/reporters/sarif.py +69 -14
- coretrace_python/semantic/scopes.py +42 -1
- coretrace_python/semantic/symbols.py +2 -1
- coretrace_python/source/__init__.py +9 -1
- coretrace_python/source/manager.py +53 -7
- coretrace_python/source/model.py +12 -1
- coretrace_python/taint/__init__.py +2 -0
- coretrace_python/taint/engine.py +114 -27
- coretrace_python/taint/models.py +27 -3
- coretrace_python/taint/templates.py +686 -77
- {coretrace_python_analyzer-0.22.0.dist-info → coretrace_python_analyzer-0.24.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.22.0.dist-info → coretrace_python_analyzer-0.24.0.dist-info}/RECORD +67 -64
- {coretrace_python_analyzer-0.22.0.dist-info → coretrace_python_analyzer-0.24.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.22.0.dist-info → coretrace_python_analyzer-0.24.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.22.0.dist-info → coretrace_python_analyzer-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.22.0.dist-info → coretrace_python_analyzer-0.24.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Module-level names whose value is known (architecture §18): the names bound once and
|
|
2
|
+
never rebound, and among them the ones bound to an immutable collection of constants,
|
|
3
|
+
which an allowlist reads."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import ClassVar
|
|
10
|
+
|
|
11
|
+
from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
|
|
12
|
+
from coretrace_python.cfg import CFGError
|
|
13
|
+
from coretrace_python.hir import nodes
|
|
14
|
+
from coretrace_python.ir.lowering import (
|
|
15
|
+
MODULE_BODY,
|
|
16
|
+
LoweringError,
|
|
17
|
+
PyIRAnalysis,
|
|
18
|
+
analyzable_functions,
|
|
19
|
+
)
|
|
20
|
+
from coretrace_python.ir.model import (
|
|
21
|
+
BuildDict,
|
|
22
|
+
BuildList,
|
|
23
|
+
BuildSet,
|
|
24
|
+
BuildTuple,
|
|
25
|
+
Call,
|
|
26
|
+
Constant,
|
|
27
|
+
ForNext,
|
|
28
|
+
Instruction,
|
|
29
|
+
SetGlobal,
|
|
30
|
+
StoreLocal,
|
|
31
|
+
Symbol,
|
|
32
|
+
Value,
|
|
33
|
+
)
|
|
34
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
35
|
+
|
|
36
|
+
FROZENSET = SymbolId("python.builtins.frozenset")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def module_bindings(ctx: AnalysisContext) -> tuple[Mapping[str, Instruction], Mapping[Value, Instruction]]:
|
|
40
|
+
"""The module-level names bound once and never rebound, by no other statement of the
|
|
41
|
+
module body, no function assigning them as ``global``, no definition of that name,
|
|
42
|
+
each with the instruction that makes its value; and the definitions of the module
|
|
43
|
+
body's values. A module that does not lower entirely has none."""
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
functions = [ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module)]
|
|
47
|
+
except (LoweringError, CFGError):
|
|
48
|
+
return MappingProxyType({}), MappingProxyType({})
|
|
49
|
+
body = next((function for function in functions if function.name == MODULE_BODY), None)
|
|
50
|
+
if body is None:
|
|
51
|
+
return MappingProxyType({}), MappingProxyType({})
|
|
52
|
+
defs = {i.result: i for block in body.blocks for i in block.instructions if i.result is not None}
|
|
53
|
+
stored: dict[str, list[Value | None]] = {}
|
|
54
|
+
for block in body.blocks:
|
|
55
|
+
for instruction in block.instructions:
|
|
56
|
+
if isinstance(instruction, StoreLocal):
|
|
57
|
+
stored.setdefault(instruction.name, []).append(instruction.value)
|
|
58
|
+
if isinstance(block.terminator, ForNext):
|
|
59
|
+
# A ``for`` binds its target to each item.
|
|
60
|
+
stored.setdefault(block.terminator.target, []).append(None)
|
|
61
|
+
rebound = {
|
|
62
|
+
i.name for function in functions for block in function.blocks for i in block.instructions
|
|
63
|
+
if isinstance(i, SetGlobal)
|
|
64
|
+
}
|
|
65
|
+
rebound |= {s.name for s in ctx.module.body if isinstance(s, nodes.Function | nodes.Class)}
|
|
66
|
+
bound: dict[str, Instruction] = {}
|
|
67
|
+
for name, values in stored.items():
|
|
68
|
+
value = values[0] if len(values) == 1 and name not in rebound else None
|
|
69
|
+
made = defs.get(value) if value is not None else None
|
|
70
|
+
if made is not None:
|
|
71
|
+
bound[name] = made
|
|
72
|
+
return MappingProxyType(bound), MappingProxyType(defs)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def constant_members(display: Instruction | None, defs: Mapping[Value, Instruction]) -> bool:
|
|
76
|
+
"""Whether ``display`` is a tuple, list or set display whose items are all constants,
|
|
77
|
+
or a dict display whose keys are, with nothing unpacked into it: the members a
|
|
78
|
+
membership test reads."""
|
|
79
|
+
|
|
80
|
+
if isinstance(display, BuildList | BuildTuple | BuildSet):
|
|
81
|
+
return not display.unpacked and all(isinstance(defs.get(e), Constant) for e in display.elements)
|
|
82
|
+
if isinstance(display, BuildDict):
|
|
83
|
+
return not display.unpacked and all(isinstance(defs.get(key), Constant) for key, _ in display.items)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def converts(call: Call, defs: Mapping[Value, Instruction], symbols: frozenset[SymbolId]) -> Value | None:
|
|
88
|
+
"""The one positional argument of ``call`` when it calls one of ``symbols`` with
|
|
89
|
+
nothing else (``frozenset(x)``), otherwise ``None``."""
|
|
90
|
+
|
|
91
|
+
callee = defs.get(call.callee)
|
|
92
|
+
if not isinstance(callee, Symbol) or callee.symbol_id not in symbols:
|
|
93
|
+
return None
|
|
94
|
+
if len(call.arguments) != 1 or call.keywords or call.starred:
|
|
95
|
+
return None
|
|
96
|
+
return call.arguments[0]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ModuleCollectionsAnalysis(Analysis[frozenset[str]]):
|
|
100
|
+
"""The module-level names bound once and never rebound (``module_bindings``) to an
|
|
101
|
+
immutable collection of constants: a tuple display, or ``frozenset`` of a display
|
|
102
|
+
whose members are constants (``constant_members``). A list, a set or a dict can still
|
|
103
|
+
change after it is bound, so it is not one."""
|
|
104
|
+
|
|
105
|
+
name: ClassVar[str] = "abstract.module_collections"
|
|
106
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def compute(cls, ctx: AnalysisContext) -> frozenset[str]:
|
|
110
|
+
bound, defs = module_bindings(ctx)
|
|
111
|
+
found: set[str] = set()
|
|
112
|
+
for name, made in bound.items():
|
|
113
|
+
collection: Instruction | None = made
|
|
114
|
+
if isinstance(made, Call):
|
|
115
|
+
argument = converts(made, defs, frozenset({FROZENSET}))
|
|
116
|
+
collection = defs.get(argument) if argument is not None else None
|
|
117
|
+
elif not isinstance(made, BuildTuple):
|
|
118
|
+
continue
|
|
119
|
+
if constant_members(collection, defs):
|
|
120
|
+
found.add(name)
|
|
121
|
+
return frozenset(found)
|
|
@@ -9,24 +9,15 @@ from collections.abc import Mapping
|
|
|
9
9
|
from types import MappingProxyType
|
|
10
10
|
from typing import ClassVar
|
|
11
11
|
|
|
12
|
+
from coretrace_python.abstract.module_constants import module_bindings
|
|
12
13
|
from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
|
|
13
|
-
from coretrace_python.
|
|
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
|
-
)
|
|
14
|
+
from coretrace_python.ir.lowering import PyIRAnalysis
|
|
21
15
|
from coretrace_python.ir.model import (
|
|
22
16
|
BinaryOp,
|
|
23
17
|
BuildString,
|
|
24
18
|
Constant,
|
|
25
|
-
ForNext,
|
|
26
19
|
Global,
|
|
27
20
|
Instruction,
|
|
28
|
-
SetGlobal,
|
|
29
|
-
StoreLocal,
|
|
30
21
|
Value,
|
|
31
22
|
)
|
|
32
23
|
|
|
@@ -59,40 +50,19 @@ def leading_text(value: Value, defs: Mapping[Value, Instruction], strings: Mappi
|
|
|
59
50
|
|
|
60
51
|
|
|
61
52
|
class ModuleStringsAnalysis(Analysis[Mapping[str, str]]):
|
|
62
|
-
"""The module-level names bound once
|
|
63
|
-
|
|
64
|
-
definition of that name. A module that does not lower entirely has none."""
|
|
53
|
+
"""The module-level names bound once and never rebound (``module_bindings``) to a
|
|
54
|
+
string literal, with that literal."""
|
|
65
55
|
|
|
66
56
|
name: ClassVar[str] = "abstract.module_strings"
|
|
67
57
|
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
|
|
68
58
|
|
|
69
59
|
@classmethod
|
|
70
60
|
def compute(cls, ctx: AnalysisContext) -> Mapping[str, str]:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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)
|
|
61
|
+
bound, _ = module_bindings(ctx)
|
|
62
|
+
return MappingProxyType(
|
|
63
|
+
{
|
|
64
|
+
name: made.value
|
|
65
|
+
for name, made in bound.items()
|
|
66
|
+
if isinstance(made, Constant) and isinstance(made.value, str)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from collections.abc import Sequence
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
6
|
from typing import ClassVar
|
|
7
7
|
|
|
8
8
|
from coretrace_python.analysis import AnyAnalysis
|
|
9
9
|
from coretrace_python.dependency import DependencyAnalysis
|
|
10
|
-
from coretrace_python.findings import Confidence, Finding, Severity
|
|
10
|
+
from coretrace_python.findings import Confidence, Finding, RuleDescription, Severity
|
|
11
11
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class DependencyPolicyPlugin(ProjectPlugin):
|
|
15
15
|
name: ClassVar[str] = "dependency-policy"
|
|
16
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
17
|
+
"denied-dependency": RuleDescription(
|
|
18
|
+
"Denied dependency",
|
|
19
|
+
"A requirement on a package the policy denies.",
|
|
20
|
+
),
|
|
21
|
+
"unpinned-dependency": RuleDescription(
|
|
22
|
+
"Unpinned dependency",
|
|
23
|
+
"A requirement without an exact pin, when the policy requires pins.",
|
|
24
|
+
),
|
|
25
|
+
}
|
|
16
26
|
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis})
|
|
17
27
|
|
|
18
28
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
@@ -4,7 +4,7 @@ template applies is a call to the function behind it, at the template's line."""
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from collections.abc import Iterator, Sequence
|
|
7
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
8
8
|
from typing import ClassVar
|
|
9
9
|
|
|
10
10
|
from coretrace_python.analysis import AnyAnalysis
|
|
@@ -17,7 +17,7 @@ from coretrace_python.dependency.correlation import (
|
|
|
17
17
|
evidence,
|
|
18
18
|
suffix_index,
|
|
19
19
|
)
|
|
20
|
-
from coretrace_python.findings import Confidence, Finding
|
|
20
|
+
from coretrace_python.findings import Confidence, Finding, RuleDescription
|
|
21
21
|
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
22
22
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
23
23
|
from coretrace_python.semantic.symbols import SymbolId
|
|
@@ -26,6 +26,12 @@ from coretrace_python.source import SourceSpan
|
|
|
26
26
|
|
|
27
27
|
class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
28
28
|
name: ClassVar[str] = "reachable-vulnerability"
|
|
29
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
30
|
+
"reachable-vulnerability": RuleDescription(
|
|
31
|
+
"Reachable dependency vulnerability",
|
|
32
|
+
"The project calls an API that an advisory of a vulnerable requirement affects.",
|
|
33
|
+
),
|
|
34
|
+
}
|
|
29
35
|
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
|
|
30
36
|
|
|
31
37
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from collections.abc import Sequence
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
6
|
from pathlib import PurePath
|
|
7
7
|
from typing import ClassVar
|
|
8
8
|
|
|
@@ -15,13 +15,19 @@ from coretrace_python.dependency.correlation import (
|
|
|
15
15
|
ruled_out,
|
|
16
16
|
suffix_index,
|
|
17
17
|
)
|
|
18
|
-
from coretrace_python.findings import Confidence, Finding
|
|
18
|
+
from coretrace_python.findings import Confidence, Finding, RuleDescription
|
|
19
19
|
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
20
20
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class VulnerableDependencyPlugin(ProjectPlugin):
|
|
24
24
|
name: ClassVar[str] = "vulnerable-dependency"
|
|
25
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
26
|
+
"vulnerable-dependency": RuleDescription(
|
|
27
|
+
"Vulnerable dependency",
|
|
28
|
+
"A requirement allows a version that an advisory affects.",
|
|
29
|
+
),
|
|
30
|
+
}
|
|
25
31
|
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
|
|
26
32
|
|
|
27
33
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
@@ -62,6 +62,33 @@ class PythonStdlibModels(ModelPlugin):
|
|
|
62
62
|
# The code is the first argument; the globals and locals it runs with are data.
|
|
63
63
|
Sink(_sym("builtins.eval"), TaintKind.CODE, ((TaintKind.CODE, (0,)),)),
|
|
64
64
|
Sink(_sym("builtins.exec"), TaintKind.CODE, ((TaintKind.CODE, (0,)),)),
|
|
65
|
+
# The module or the file the call imports or runs; its globals, its run name and
|
|
66
|
+
# the package of a relative name are data (#240).
|
|
67
|
+
*(
|
|
68
|
+
Sink(_sym(symbol), TaintKind.CODE, ((TaintKind.CODE, (0,)),), ((TaintKind.CODE, (keyword,)),), selects=True)
|
|
69
|
+
for symbol, keyword in (
|
|
70
|
+
("importlib.import_module", "name"),
|
|
71
|
+
("builtins.__import__", "name"),
|
|
72
|
+
("importlib.__import__", "name"),
|
|
73
|
+
("runpy.run_module", "mod_name"),
|
|
74
|
+
("runpy.run_path", "path_name"),
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
# The directory imports are looked up in, and the site directory whose ``.pth``
|
|
78
|
+
# files run; the index of ``insert`` is data (#240).
|
|
79
|
+
Sink(_sym("sys.path.insert"), TaintKind.CODE, ((TaintKind.CODE, (1,)),), selects=True),
|
|
80
|
+
Sink(_sym("sys.path.append"), TaintKind.CODE, ((TaintKind.CODE, (0,)),), selects=True),
|
|
81
|
+
Sink(_sym("sys.path.extend"), TaintKind.CODE, ((TaintKind.CODE, (0,)),), selects=True),
|
|
82
|
+
Sink(_sym("site.addsitedir"), TaintKind.CODE, ((TaintKind.CODE, (0,)),), ((TaintKind.CODE, ("sitedir",)),), selects=True),
|
|
83
|
+
# The name of the attribute ``getattr`` selects, when what it selects is called;
|
|
84
|
+
# the object and the default are data (#240).
|
|
85
|
+
Sink(
|
|
86
|
+
_sym("builtins.getattr"),
|
|
87
|
+
TaintKind.CODE,
|
|
88
|
+
((TaintKind.CODE, (1,)),),
|
|
89
|
+
selects=True,
|
|
90
|
+
requires_called_result=TaintKind.CODE,
|
|
91
|
+
),
|
|
65
92
|
Sink(_sym("builtins.open"), TaintKind.PATH),
|
|
66
93
|
Sink(_sym("os.remove"), TaintKind.PATH),
|
|
67
94
|
Sink(_sym("os.unlink"), TaintKind.PATH),
|
|
@@ -9,6 +9,7 @@ from collections.abc import Sequence
|
|
|
9
9
|
from typing import ClassVar
|
|
10
10
|
|
|
11
11
|
from coretrace_python.findings import Finding
|
|
12
|
+
from coretrace_python.findings.context import path_in_project
|
|
12
13
|
from coretrace_python.plugins import PluginContext, ProjectContext, ProjectPlugin
|
|
13
14
|
from coretrace_python.plugins.secrets import (
|
|
14
15
|
DEFAULT_CREDENTIAL_NAMES,
|
|
@@ -32,7 +33,7 @@ class ConfigSecrets(SecretDetector, ProjectPlugin):
|
|
|
32
33
|
return ()
|
|
33
34
|
findings: list[Finding] = []
|
|
34
35
|
for value, name, span, function in config_literals(ctx.root):
|
|
35
|
-
finding = self.judge(value, name, span, function)
|
|
36
|
+
finding = self.judge(value, name, span, function, path=path_in_project(span.source_id, ctx.root))
|
|
36
37
|
if finding is not None:
|
|
37
38
|
findings.append(finding)
|
|
38
39
|
return findings
|
|
@@ -1,19 +1,107 @@
|
|
|
1
1
|
"""Code injection: attacker-controlled input reaching a CODE sink, such as the code
|
|
2
|
-
``eval`` or ``exec`` runs. ``
|
|
3
|
-
the
|
|
2
|
+
``eval`` or ``exec`` runs, the module ``importlib.import_module``, ``__import__`` or
|
|
3
|
+
``runpy.run_module`` imports, the file ``runpy.run_path`` runs, the directory
|
|
4
|
+
``sys.path`` and ``site.addsitedir`` load code from, or the attribute ``getattr`` selects
|
|
5
|
+
when it is called. ``dangerous-eval`` reports every ``eval`` or ``exec``; this rule reports
|
|
6
|
+
the calls attacker input reaches.
|
|
7
|
+
|
|
8
|
+
An import runs the top-level code of a module the input chooses among those that can be
|
|
9
|
+
found, and a script path runs the file the input chooses: execution is possible, and
|
|
10
|
+
how far it goes depends on what the input can reach. Their findings are titled for
|
|
11
|
+
that. A constant package prefix (``"app.plugins." + name``) confines an import to the
|
|
12
|
+
modules of that package: a hotspot. A directory added to ``sys.path`` runs code only
|
|
13
|
+
when a later import resolves there: a hotspot. ``site.addsitedir`` runs the import lines
|
|
14
|
+
of the directory's ``.pth`` files during the call: a vulnerability, exploitable when the
|
|
15
|
+
directory holds files the attacker controls, as the finding states. The attribute
|
|
16
|
+
``getattr`` selects runs at each call of its result, where the finding is placed: what
|
|
17
|
+
runs depends on what the object exposes, a hotspot."""
|
|
4
18
|
|
|
5
19
|
from __future__ import annotations
|
|
6
20
|
|
|
21
|
+
from collections.abc import Mapping
|
|
7
22
|
from typing import ClassVar
|
|
8
23
|
|
|
9
|
-
from coretrace_python.
|
|
10
|
-
from coretrace_python.
|
|
11
|
-
from coretrace_python.
|
|
24
|
+
from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
|
|
25
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
26
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
27
|
+
from coretrace_python.findings.refutation import Status, Verdict
|
|
28
|
+
from coretrace_python.hir import nodes
|
|
29
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
30
|
+
from coretrace_python.plugins import Assessment, PluginContext, TaintDetector
|
|
31
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
32
|
+
from coretrace_python.taint import TaintFlow, TaintKind
|
|
33
|
+
|
|
34
|
+
_IMPORTS = frozenset(
|
|
35
|
+
SymbolId(f"python.{name}")
|
|
36
|
+
for name in ("importlib.import_module", "builtins.__import__", "importlib.__import__", "runpy.run_module")
|
|
37
|
+
)
|
|
38
|
+
_RUN_PATH = SymbolId("python.runpy.run_path")
|
|
39
|
+
_IMPORT_PATHS = frozenset(SymbolId(f"python.sys.path.{name}") for name in ("insert", "append", "extend"))
|
|
40
|
+
_SITE_DIRECTORY = SymbolId("python.site.addsitedir")
|
|
41
|
+
_GETATTR = SymbolId("python.builtins.getattr")
|
|
12
42
|
|
|
13
43
|
|
|
14
44
|
class CodeInjectionPlugin(TaintDetector):
|
|
15
45
|
name: ClassVar[str] = "code-injection"
|
|
46
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
47
|
+
"code-injection": RuleDescription(
|
|
48
|
+
"Code injection",
|
|
49
|
+
"Attacker-controlled data chooses code that runs: the code eval or exec runs, the module an import loads, the script runpy runs, or the attribute getattr selects and the program calls.",
|
|
50
|
+
),
|
|
51
|
+
}
|
|
16
52
|
rule_id: ClassVar[str] = "code-injection"
|
|
17
53
|
kind: ClassVar[TaintKind] = TaintKind.CODE
|
|
18
54
|
severity: ClassVar[Severity] = Severity.HIGH
|
|
19
55
|
title: ClassVar[str] = "Code injection"
|
|
56
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
|
|
57
|
+
|
|
58
|
+
def assess(
|
|
59
|
+
self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict
|
|
60
|
+
) -> Assessment:
|
|
61
|
+
symbol = flow.sink.symbol
|
|
62
|
+
if symbol == _RUN_PATH:
|
|
63
|
+
return Assessment(verdict, self.severity, "Untrusted script execution")
|
|
64
|
+
if symbol in _IMPORT_PATHS:
|
|
65
|
+
if verdict.status is Status.VULNERABILITY:
|
|
66
|
+
verdict = Verdict(
|
|
67
|
+
flow,
|
|
68
|
+
Status.HOTSPOT,
|
|
69
|
+
"the input chooses a directory imports are looked up in: code runs only when a later "
|
|
70
|
+
"import resolves to a module the attacker placed there",
|
|
71
|
+
)
|
|
72
|
+
return Assessment(verdict, self.severity, "Untrusted import path")
|
|
73
|
+
if symbol == _GETATTR:
|
|
74
|
+
if verdict.status is Status.VULNERABILITY:
|
|
75
|
+
verdict = Verdict(
|
|
76
|
+
flow,
|
|
77
|
+
Status.HOTSPOT,
|
|
78
|
+
"the input chooses the attribute called: what runs depends on what the object exposes",
|
|
79
|
+
)
|
|
80
|
+
lines: dict[str, str] = {}
|
|
81
|
+
if flow.invoked_at is not None and flow.sink_location is not None:
|
|
82
|
+
lines = {
|
|
83
|
+
"selected_line": str(flow.sink_location.start_line),
|
|
84
|
+
"invoked_line": str(flow.invoked_at.start_line),
|
|
85
|
+
}
|
|
86
|
+
return Assessment(verdict, self.severity, "Untrusted attribute call", lines)
|
|
87
|
+
if symbol == _SITE_DIRECTORY:
|
|
88
|
+
if verdict.status is Status.VULNERABILITY:
|
|
89
|
+
verdict = Verdict(
|
|
90
|
+
flow,
|
|
91
|
+
Status.VULNERABILITY,
|
|
92
|
+
f"{verdict.evidence}; the import lines of the .pth files of the directory run during the "
|
|
93
|
+
"call: exploitable when the input chooses a directory holding files the attacker controls",
|
|
94
|
+
)
|
|
95
|
+
return Assessment(verdict, self.severity, "Untrusted site directory")
|
|
96
|
+
if symbol not in _IMPORTS:
|
|
97
|
+
return Assessment(verdict, self.severity)
|
|
98
|
+
if verdict.status is Status.VULNERABILITY and flow.through is None:
|
|
99
|
+
ssa = ctx.get(SSAAnalysis, function)
|
|
100
|
+
defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
|
|
101
|
+
prefix, whole = leading_text(flow.argument, defs, ctx.get(ModuleStringsAnalysis))
|
|
102
|
+
package = prefix.rpartition(".")[0]
|
|
103
|
+
if not whole and package and not prefix.startswith("."):
|
|
104
|
+
verdict = Verdict(
|
|
105
|
+
flow, Status.HOTSPOT, f"the package is fixed by {prefix!r}, but the input chooses a module in it"
|
|
106
|
+
)
|
|
107
|
+
return Assessment(verdict, self.severity, "Untrusted module import")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name = "code-injection"
|
|
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.code-injection"]
|
|
6
6
|
|
|
7
7
|
[entrypoint]
|
|
@@ -36,7 +36,7 @@ from typing import ClassVar
|
|
|
36
36
|
|
|
37
37
|
from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
|
|
38
38
|
from coretrace_python.analysis import AnyAnalysis
|
|
39
|
-
from coretrace_python.findings import Severity
|
|
39
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
40
40
|
from coretrace_python.findings.refutation import Status, Verdict
|
|
41
41
|
from coretrace_python.hir import nodes
|
|
42
42
|
from coretrace_python.interprocedural import Arguments
|
|
@@ -398,6 +398,12 @@ class _Operands:
|
|
|
398
398
|
|
|
399
399
|
class CommandInjectionPlugin(TaintDetector):
|
|
400
400
|
name: ClassVar[str] = "command-injection"
|
|
401
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
402
|
+
"command-injection": RuleDescription(
|
|
403
|
+
"Command injection",
|
|
404
|
+
"Attacker-controlled data reaches a shell or process execution (os.system, subprocess, ...) where it can run or choose a command.",
|
|
405
|
+
),
|
|
406
|
+
}
|
|
401
407
|
rule_id: ClassVar[str] = "command-injection"
|
|
402
408
|
kind: ClassVar[TaintKind] = TaintKind.COMMAND
|
|
403
409
|
severity: ClassVar[Severity] = Severity.HIGH
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
from typing import ClassVar
|
|
6
7
|
|
|
7
|
-
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
8
9
|
from coretrace_python.plugins import TaintDetector
|
|
9
10
|
from coretrace_python.taint import TaintKind
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class InsecureDeserializationPlugin(TaintDetector):
|
|
13
14
|
name: ClassVar[str] = "insecure-deserialization"
|
|
15
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
16
|
+
"insecure-deserialization": RuleDescription(
|
|
17
|
+
"Insecure deserialization",
|
|
18
|
+
"Attacker-controlled data reaches a deserializer that can build arbitrary objects or run code: pickle, marshal, dill, jsonpickle, shelve, or a YAML loader without a safe loader.",
|
|
19
|
+
),
|
|
20
|
+
}
|
|
14
21
|
rule_id: ClassVar[str] = "insecure-deserialization"
|
|
15
22
|
kind: ClassVar[TaintKind] = TaintKind.DESERIALIZATION
|
|
16
23
|
severity: ClassVar[Severity] = Severity.CRITICAL
|
|
@@ -5,15 +5,22 @@ sink; model plugins declare them."""
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
from typing import ClassVar
|
|
9
10
|
|
|
10
|
-
from coretrace_python.findings import Severity
|
|
11
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
11
12
|
from coretrace_python.plugins import TaintDetector
|
|
12
13
|
from coretrace_python.taint import TaintKind
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class NoSqlInjectionPlugin(TaintDetector):
|
|
16
17
|
name: ClassVar[str] = "nosql-injection"
|
|
18
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
19
|
+
"nosql-injection": RuleDescription(
|
|
20
|
+
"NoSQL injection",
|
|
21
|
+
"Attacker input that may hold a structure, such as a JSON body, reaches a NoSQL query, where an operator such as {\"$ne\": null} matches any value.",
|
|
22
|
+
),
|
|
23
|
+
}
|
|
17
24
|
rule_id: ClassVar[str] = "nosql-injection"
|
|
18
25
|
kind: ClassVar[TaintKind] = TaintKind.NOSQL
|
|
19
26
|
severity: ClassVar[Severity] = Severity.HIGH
|
|
@@ -10,11 +10,12 @@ unproven, ``"/" + next`` included, stays a vulnerability.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
from collections.abc import Mapping
|
|
13
14
|
from typing import ClassVar
|
|
14
15
|
|
|
15
16
|
from coretrace_python.abstract.strings import ModuleStringsAnalysis
|
|
16
17
|
from coretrace_python.analysis import AnyAnalysis
|
|
17
|
-
from coretrace_python.findings import Severity
|
|
18
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
18
19
|
from coretrace_python.findings.refutation import Status, Verdict
|
|
19
20
|
from coretrace_python.hir import nodes
|
|
20
21
|
from coretrace_python.ir.ssa import SSAAnalysis
|
|
@@ -25,6 +26,12 @@ from coretrace_python.taint.urls import flow_url
|
|
|
25
26
|
|
|
26
27
|
class OpenRedirectPlugin(TaintDetector):
|
|
27
28
|
name: ClassVar[str] = "open-redirect"
|
|
29
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
30
|
+
"open-redirect": RuleDescription(
|
|
31
|
+
"Open redirect",
|
|
32
|
+
"Attacker-controlled data chooses the target of an HTTP redirect, which can send the user to another site.",
|
|
33
|
+
),
|
|
34
|
+
}
|
|
28
35
|
rule_id: ClassVar[str] = "open-redirect"
|
|
29
36
|
kind: ClassVar[TaintKind] = TaintKind.REDIRECT
|
|
30
37
|
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
from typing import ClassVar
|
|
6
7
|
|
|
7
|
-
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
8
9
|
from coretrace_python.plugins import TaintDetector
|
|
9
10
|
from coretrace_python.taint import TaintKind
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class PathTraversalPlugin(TaintDetector):
|
|
13
14
|
name: ClassVar[str] = "path-traversal"
|
|
15
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
16
|
+
"path-traversal": RuleDescription(
|
|
17
|
+
"Path traversal",
|
|
18
|
+
"Attacker-controlled data reaches a file system path (open, send_file, os.remove, ...).",
|
|
19
|
+
),
|
|
20
|
+
}
|
|
14
21
|
rule_id: ClassVar[str] = "path-traversal"
|
|
15
22
|
kind: ClassVar[TaintKind] = TaintKind.PATH
|
|
16
23
|
severity: ClassVar[Severity] = Severity.HIGH
|
|
@@ -5,15 +5,22 @@ a hint, so medium confidence."""
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
from typing import ClassVar
|
|
9
10
|
|
|
10
|
-
from coretrace_python.findings import Confidence, Severity
|
|
11
|
+
from coretrace_python.findings import Confidence, RuleDescription, Severity
|
|
11
12
|
from coretrace_python.plugins import TaintDetector
|
|
12
13
|
from coretrace_python.taint import TaintKind
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class PlaintextCredentialsPlugin(TaintDetector):
|
|
16
17
|
name: ClassVar[str] = "plaintext-credentials"
|
|
18
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
19
|
+
"plaintext-credential-storage": RuleDescription(
|
|
20
|
+
"Plaintext credential storage",
|
|
21
|
+
"A parameter named like a password is stored in a database without hashing.",
|
|
22
|
+
),
|
|
23
|
+
}
|
|
17
24
|
rule_id: ClassVar[str] = "plaintext-credential-storage"
|
|
18
25
|
kind: ClassVar[TaintKind] = TaintKind.CREDENTIAL
|
|
19
26
|
severity: ClassVar[Severity] = Severity.HIGH
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
from typing import ClassVar
|
|
6
7
|
|
|
7
|
-
from coretrace_python.findings import Severity
|
|
8
|
+
from coretrace_python.findings import RuleDescription, Severity
|
|
8
9
|
from coretrace_python.plugins import TaintDetector
|
|
9
10
|
from coretrace_python.taint import TaintKind
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class SqlInjectionPlugin(TaintDetector):
|
|
13
14
|
name: ClassVar[str] = "sql-injection"
|
|
15
|
+
rules: ClassVar[Mapping[str, RuleDescription]] = {
|
|
16
|
+
"sql-injection": RuleDescription(
|
|
17
|
+
"SQL injection",
|
|
18
|
+
"Attacker-controlled data reaches a database statement (cursor.execute, SQLAlchemy text, Django raw, ...) rather than a query parameter.",
|
|
19
|
+
),
|
|
20
|
+
}
|
|
14
21
|
rule_id: ClassVar[str] = "sql-injection"
|
|
15
22
|
kind: ClassVar[TaintKind] = TaintKind.SQL
|
|
16
23
|
severity: ClassVar[Severity] = Severity.HIGH
|