coretrace-python-analyzer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coretrace_python/__init__.py +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Calls, anywhere in the project, to an API affected by a vulnerable requirement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
+
from coretrace_python.dependency import DependencyAnalysis
|
|
10
|
+
from coretrace_python.findings import Confidence, Finding
|
|
11
|
+
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
12
|
+
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
16
|
+
name: ClassVar[str] = "reachable-vulnerability"
|
|
17
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
|
|
18
|
+
|
|
19
|
+
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
20
|
+
affected = {}
|
|
21
|
+
for requirement in ctx.dependencies.requirements:
|
|
22
|
+
for advisory in ctx.advisories:
|
|
23
|
+
if advisory.affects(requirement):
|
|
24
|
+
for symbol in advisory.affected_symbols:
|
|
25
|
+
affected.setdefault(symbol, advisory)
|
|
26
|
+
if not affected:
|
|
27
|
+
return ()
|
|
28
|
+
findings: list[Finding] = []
|
|
29
|
+
for module in ctx.modules:
|
|
30
|
+
graph = ctx.call_graph(module)
|
|
31
|
+
for function in graph.functions:
|
|
32
|
+
for site in graph.sites(function):
|
|
33
|
+
if not isinstance(site.target, ExternalSymbol):
|
|
34
|
+
continue
|
|
35
|
+
advisory = affected.get(site.target.symbol)
|
|
36
|
+
if advisory is None:
|
|
37
|
+
continue
|
|
38
|
+
findings.append(
|
|
39
|
+
Finding(
|
|
40
|
+
rule_id="reachable-vulnerability",
|
|
41
|
+
message=(
|
|
42
|
+
f"{advisory.id}: {site.target.symbol} is affected in the required "
|
|
43
|
+
f"{advisory.package} {advisory.vulnerable}: {advisory.summary}"
|
|
44
|
+
),
|
|
45
|
+
severity=advisory.severity,
|
|
46
|
+
confidence=Confidence.HIGH,
|
|
47
|
+
span=site.location,
|
|
48
|
+
function=function,
|
|
49
|
+
metadata={
|
|
50
|
+
"advisory": advisory.id,
|
|
51
|
+
"package": advisory.package,
|
|
52
|
+
"symbol": str(site.target.symbol),
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
return findings
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""A small, offline sample of published advisories (architecture §26).
|
|
2
|
+
|
|
3
|
+
This is a demonstration database: a handful of well-known CVEs with the APIs they
|
|
4
|
+
affect. A live OSV or GHSA feed belongs to a dedicated plugin.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import ClassVar
|
|
10
|
+
|
|
11
|
+
from coretrace_python.dependency import Advisory
|
|
12
|
+
from coretrace_python.findings import Severity
|
|
13
|
+
from coretrace_python.plugins import ModelPlugin
|
|
14
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _sym(path: str) -> SymbolId:
|
|
18
|
+
return SymbolId(f"python.{path}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SampleAdvisories(ModelPlugin):
|
|
22
|
+
name: ClassVar[str] = "sample-advisories"
|
|
23
|
+
advisories: ClassVar[tuple[Advisory, ...]] = (
|
|
24
|
+
Advisory(
|
|
25
|
+
"CVE-2020-1747",
|
|
26
|
+
"pyyaml",
|
|
27
|
+
"<5.4",
|
|
28
|
+
"yaml.load and full_load can execute arbitrary code from untrusted documents",
|
|
29
|
+
Severity.CRITICAL,
|
|
30
|
+
(_sym("yaml.load"), _sym("yaml.full_load"), _sym("yaml.unsafe_load")),
|
|
31
|
+
),
|
|
32
|
+
Advisory(
|
|
33
|
+
"CVE-2018-18074",
|
|
34
|
+
"requests",
|
|
35
|
+
"<2.20.0",
|
|
36
|
+
"Authorization header is leaked on redirects to another host",
|
|
37
|
+
Severity.HIGH,
|
|
38
|
+
(_sym("requests.get"), _sym("requests.post"), _sym("requests.request"), _sym("requests.Session")),
|
|
39
|
+
),
|
|
40
|
+
Advisory(
|
|
41
|
+
"CVE-2020-28493",
|
|
42
|
+
"jinja2",
|
|
43
|
+
"<2.11.3",
|
|
44
|
+
"regular expression denial of service in the urlize filter",
|
|
45
|
+
Severity.MEDIUM,
|
|
46
|
+
(_sym("jinja2.Environment"), _sym("jinja2.Template")),
|
|
47
|
+
),
|
|
48
|
+
Advisory(
|
|
49
|
+
"CVE-2021-33503",
|
|
50
|
+
"urllib3",
|
|
51
|
+
"<1.26.5",
|
|
52
|
+
"regular expression denial of service when parsing authority in URLs",
|
|
53
|
+
Severity.HIGH,
|
|
54
|
+
(_sym("urllib3.PoolManager"), _sym("urllib3.request")),
|
|
55
|
+
),
|
|
56
|
+
Advisory(
|
|
57
|
+
"CVE-2023-30861",
|
|
58
|
+
"flask",
|
|
59
|
+
"<2.2.5",
|
|
60
|
+
"session cookie may be leaked through caching proxies",
|
|
61
|
+
Severity.HIGH,
|
|
62
|
+
(_sym("flask.Flask"),),
|
|
63
|
+
),
|
|
64
|
+
Advisory(
|
|
65
|
+
"CVE-2024-1135",
|
|
66
|
+
"gunicorn",
|
|
67
|
+
"<22.0.0",
|
|
68
|
+
"HTTP request smuggling through improper Transfer-Encoding validation",
|
|
69
|
+
Severity.HIGH,
|
|
70
|
+
(),
|
|
71
|
+
("GHSA-w3h3-4rj7-4ph4",),
|
|
72
|
+
),
|
|
73
|
+
Advisory(
|
|
74
|
+
"CVE-2023-4863",
|
|
75
|
+
"opencv-python",
|
|
76
|
+
"<4.8.1.78",
|
|
77
|
+
"heap buffer overflow in the bundled libwebp when decoding a crafted WebP image",
|
|
78
|
+
Severity.CRITICAL,
|
|
79
|
+
(_sym("cv2.imread"), _sym("cv2.imdecode"), _sym("cv2.imreadmulti")),
|
|
80
|
+
("GHSA-qr4w-53vh-m672",),
|
|
81
|
+
),
|
|
82
|
+
Advisory(
|
|
83
|
+
"CVE-2024-34069",
|
|
84
|
+
"werkzeug",
|
|
85
|
+
"<3.0.3",
|
|
86
|
+
"the debugger can be tricked into executing code through a crafted domain",
|
|
87
|
+
Severity.HIGH,
|
|
88
|
+
(_sym("werkzeug.debug.DebuggedApplication"),),
|
|
89
|
+
("GHSA-2g68-c3qc-8985",),
|
|
90
|
+
),
|
|
91
|
+
Advisory(
|
|
92
|
+
"CVE-2023-46136",
|
|
93
|
+
"werkzeug",
|
|
94
|
+
"<3.0.1",
|
|
95
|
+
"a crafted multipart body exhausts CPU and memory in the form parser",
|
|
96
|
+
Severity.HIGH,
|
|
97
|
+
(_sym("werkzeug.formparser.MultiPartParser"),),
|
|
98
|
+
("GHSA-hrfv-mqp8-q5rw",),
|
|
99
|
+
),
|
|
100
|
+
Advisory(
|
|
101
|
+
"CVE-2026-25990",
|
|
102
|
+
"pillow",
|
|
103
|
+
"<=11.2.1",
|
|
104
|
+
"out-of-bounds write when loading a crafted PSD image",
|
|
105
|
+
Severity.HIGH,
|
|
106
|
+
(_sym("PIL.Image.open"),),
|
|
107
|
+
("GHSA-cfh3-3jmp-rvhc",),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Requirements that allow a version an advisory marks as vulnerable."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
+
from coretrace_python.dependency import DependencyAnalysis
|
|
10
|
+
from coretrace_python.findings import Confidence, Finding
|
|
11
|
+
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VulnerableDependencyPlugin(ProjectPlugin):
|
|
15
|
+
name: ClassVar[str] = "vulnerable-dependency"
|
|
16
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis})
|
|
17
|
+
|
|
18
|
+
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
19
|
+
findings: list[Finding] = []
|
|
20
|
+
for requirement in ctx.dependencies.requirements:
|
|
21
|
+
for advisory in ctx.advisories:
|
|
22
|
+
if not advisory.affects(requirement):
|
|
23
|
+
continue
|
|
24
|
+
pinned = requirement.pinned is not None
|
|
25
|
+
findings.append(
|
|
26
|
+
Finding(
|
|
27
|
+
rule_id="vulnerable-dependency",
|
|
28
|
+
message=(
|
|
29
|
+
f"{advisory.id}: {advisory.package} {advisory.vulnerable} is "
|
|
30
|
+
f"{'required' if pinned else 'allowed'} by {requirement.name}"
|
|
31
|
+
f"{requirement.specifier or ''}: {advisory.summary}"
|
|
32
|
+
),
|
|
33
|
+
severity=advisory.severity,
|
|
34
|
+
confidence=Confidence.HIGH if pinned else Confidence.MEDIUM,
|
|
35
|
+
span=requirement.span,
|
|
36
|
+
metadata={
|
|
37
|
+
"advisory": advisory.id,
|
|
38
|
+
"package": advisory.package,
|
|
39
|
+
"specifier": requirement.specifier,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return findings
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Command-line entry points: the parameters of click and Typer commands are argv input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.plugins import ModelPlugin
|
|
8
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
9
|
+
from coretrace_python.taint import EntryPoint, Model, TaintKind
|
|
10
|
+
|
|
11
|
+
# A command-line tool is expected to open the paths it is given: argv input carries every
|
|
12
|
+
# kind but PATH.
|
|
13
|
+
ARGV_KINDS = TaintKind.ALL & ~TaintKind.PATH
|
|
14
|
+
|
|
15
|
+
_COMMANDS = (
|
|
16
|
+
"click.command",
|
|
17
|
+
"click.group",
|
|
18
|
+
"click.command.command",
|
|
19
|
+
"click.group.command",
|
|
20
|
+
"click.group.group",
|
|
21
|
+
"click.Group.command",
|
|
22
|
+
"typer.Typer.command",
|
|
23
|
+
"typer.Typer.callback",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sym(path: str) -> SymbolId:
|
|
28
|
+
return SymbolId(f"python.{path}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CliModels(ModelPlugin):
|
|
32
|
+
name: ClassVar[str] = "cli-models"
|
|
33
|
+
models: ClassVar[tuple[Model, ...]] = tuple(
|
|
34
|
+
EntryPoint(_sym(command), "argv", ARGV_KINDS) for command in _COMMANDS
|
|
35
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Credential models: password-named parameters are credentials, password hashing clears
|
|
2
|
+
them, and the database write sinks of the other model plugins also carry the
|
|
3
|
+
``CREDENTIAL`` kind, so a credential stored unhashed is a flow."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from coretrace_python.plugins import ModelPlugin
|
|
10
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
11
|
+
from coretrace_python.taint import Model, NamedParameter, Sanitizer, TaintKind
|
|
12
|
+
|
|
13
|
+
_HASHERS = (
|
|
14
|
+
"hashlib.pbkdf2_hmac",
|
|
15
|
+
"hashlib.scrypt",
|
|
16
|
+
"hashlib.sha256",
|
|
17
|
+
"hashlib.sha512",
|
|
18
|
+
"hashlib.sha3_256",
|
|
19
|
+
"hashlib.blake2b",
|
|
20
|
+
"hashlib.new",
|
|
21
|
+
"bcrypt.hashpw",
|
|
22
|
+
"bcrypt.kdf",
|
|
23
|
+
"argon2.PasswordHasher.hash",
|
|
24
|
+
"argon2.low_level.hash_secret",
|
|
25
|
+
"werkzeug.security.generate_password_hash",
|
|
26
|
+
"passlib.hash.pbkdf2_sha256.hash",
|
|
27
|
+
"passlib.hash.bcrypt.hash",
|
|
28
|
+
"passlib.hash.argon2.hash",
|
|
29
|
+
"passlib.context.CryptContext.hash",
|
|
30
|
+
"django.contrib.auth.hashers.make_password",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sym(path: str) -> SymbolId:
|
|
35
|
+
return SymbolId(f"python.{path}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CredentialModels(ModelPlugin):
|
|
39
|
+
name: ClassVar[str] = "credential-models"
|
|
40
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
41
|
+
NamedParameter(r"(?i)^(password|passwd|pwd|new_password|plain_password|secret)$", "credential", TaintKind.CREDENTIAL),
|
|
42
|
+
*(Sanitizer(_sym(hasher), TaintKind.CREDENTIAL) for hasher in _HASHERS),
|
|
43
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Django security models: typed requests, views, decorators, ORM and HTML sinks.
|
|
2
|
+
|
|
3
|
+
Django views are undecorated, so HTTP input comes from three generic mechanisms: a
|
|
4
|
+
parameter annotated with a request class, a method of a class-based view, or a view
|
|
5
|
+
decorated by one of Django's or Django REST framework's view decorators. A view with a
|
|
6
|
+
bare ``request`` parameter and none of these is not recognised; URL configurations are
|
|
7
|
+
not read.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
from coretrace_python.plugins import ModelPlugin
|
|
15
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
16
|
+
from coretrace_python.taint import (
|
|
17
|
+
AuthorizationGuard,
|
|
18
|
+
EntryPoint,
|
|
19
|
+
Model,
|
|
20
|
+
RouteRegistrar,
|
|
21
|
+
Sanitizer,
|
|
22
|
+
Sink,
|
|
23
|
+
SuffixSink,
|
|
24
|
+
TaintKind,
|
|
25
|
+
TypedParameter,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_REQUEST_CLASSES = (
|
|
29
|
+
"django.http.HttpRequest",
|
|
30
|
+
"django.http.request.HttpRequest",
|
|
31
|
+
"django.core.handlers.wsgi.WSGIRequest",
|
|
32
|
+
"django.core.handlers.asgi.ASGIRequest",
|
|
33
|
+
"rest_framework.request.Request",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_VIEW_BASES = (
|
|
37
|
+
"django.views.View",
|
|
38
|
+
"django.views.generic.View",
|
|
39
|
+
"django.views.generic.base.View",
|
|
40
|
+
"django.views.generic.TemplateView",
|
|
41
|
+
"django.views.generic.RedirectView",
|
|
42
|
+
"django.views.generic.ListView",
|
|
43
|
+
"django.views.generic.DetailView",
|
|
44
|
+
"django.views.generic.FormView",
|
|
45
|
+
"django.views.generic.CreateView",
|
|
46
|
+
"django.views.generic.UpdateView",
|
|
47
|
+
"django.views.generic.DeleteView",
|
|
48
|
+
"rest_framework.views.APIView",
|
|
49
|
+
"rest_framework.generics.GenericAPIView",
|
|
50
|
+
"rest_framework.viewsets.ViewSet",
|
|
51
|
+
"rest_framework.viewsets.GenericViewSet",
|
|
52
|
+
"rest_framework.viewsets.ModelViewSet",
|
|
53
|
+
"rest_framework.viewsets.ReadOnlyModelViewSet",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_VIEW_DECORATORS = (
|
|
57
|
+
"django.views.decorators.csrf.csrf_exempt",
|
|
58
|
+
"django.views.decorators.csrf.csrf_protect",
|
|
59
|
+
"django.views.decorators.http.require_http_methods",
|
|
60
|
+
"django.views.decorators.http.require_GET",
|
|
61
|
+
"django.views.decorators.http.require_POST",
|
|
62
|
+
"django.views.decorators.http.require_safe",
|
|
63
|
+
"django.views.decorators.cache.never_cache",
|
|
64
|
+
"django.views.decorators.cache.cache_page",
|
|
65
|
+
"django.contrib.auth.decorators.login_required",
|
|
66
|
+
"django.contrib.auth.decorators.permission_required",
|
|
67
|
+
"django.contrib.auth.decorators.user_passes_test",
|
|
68
|
+
"rest_framework.decorators.api_view",
|
|
69
|
+
"rest_framework.decorators.action",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
_ROUTE_REGISTRARS = (
|
|
73
|
+
"django.urls.path",
|
|
74
|
+
"django.urls.re_path",
|
|
75
|
+
"django.conf.urls.url",
|
|
76
|
+
"rest_framework.routers.DefaultRouter.register",
|
|
77
|
+
"rest_framework.routers.SimpleRouter.register",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
_AUTHORIZATION_DECORATORS = (
|
|
81
|
+
("django.contrib.auth.decorators.login_required", "login"),
|
|
82
|
+
("django.contrib.auth.decorators.permission_required", "permission"),
|
|
83
|
+
("django.contrib.auth.decorators.user_passes_test", "test"),
|
|
84
|
+
("django.contrib.admin.views.decorators.staff_member_required", "staff"),
|
|
85
|
+
("rest_framework.decorators.permission_classes", "permission"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _sym(path: str) -> SymbolId:
|
|
90
|
+
return SymbolId(f"python.{path}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DjangoModels(ModelPlugin):
|
|
94
|
+
name: ClassVar[str] = "django-models"
|
|
95
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
96
|
+
*(TypedParameter(_sym(cls), "http") for cls in _REQUEST_CLASSES),
|
|
97
|
+
*(EntryPoint(_sym(base), "http") for base in _VIEW_BASES),
|
|
98
|
+
*(EntryPoint(_sym(decorator), "http") for decorator in _VIEW_DECORATORS),
|
|
99
|
+
Sink(_sym("django.db.connection.cursor.execute"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),)),
|
|
100
|
+
Sink(_sym("django.db.connection.cursor.executemany"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),)),
|
|
101
|
+
Sink(_sym("django.db.models.expressions.RawSQL"), TaintKind.SQL | TaintKind.CREDENTIAL),
|
|
102
|
+
Sink(_sym("django.db.models.Manager.raw"), TaintKind.SQL | TaintKind.CREDENTIAL),
|
|
103
|
+
Sink(_sym("django.db.models.QuerySet.raw"), TaintKind.SQL | TaintKind.CREDENTIAL),
|
|
104
|
+
Sink(_sym("django.db.models.QuerySet.extra"), TaintKind.SQL | TaintKind.CREDENTIAL),
|
|
105
|
+
Sink(_sym("django.utils.safestring.mark_safe"), TaintKind.HTML),
|
|
106
|
+
Sink(_sym("django.http.HttpResponse"), TaintKind.HTML),
|
|
107
|
+
Sink(_sym("django.http.response.HttpResponse"), TaintKind.HTML),
|
|
108
|
+
Sink(_sym("django.template.Template"), TaintKind.HTML),
|
|
109
|
+
Sink(_sym("django.http.FileResponse"), TaintKind.PATH),
|
|
110
|
+
Sink(_sym("django.shortcuts.redirect"), TaintKind.REDIRECT),
|
|
111
|
+
Sink(_sym("django.http.HttpResponseRedirect"), TaintKind.REDIRECT),
|
|
112
|
+
Sink(_sym("django.http.HttpResponsePermanentRedirect"), TaintKind.REDIRECT),
|
|
113
|
+
Sanitizer(_sym("django.utils.html.escape"), TaintKind.HTML),
|
|
114
|
+
Sanitizer(_sym("django.utils.html.conditional_escape"), TaintKind.HTML),
|
|
115
|
+
*(AuthorizationGuard(_sym(decorator), label) for decorator, label in _AUTHORIZATION_DECORATORS),
|
|
116
|
+
# ``urlpatterns = [path('login/', views.log_in)]``: the referenced view is an
|
|
117
|
+
# entry point wherever it is defined; routers register viewsets.
|
|
118
|
+
*(RouteRegistrar(_sym(registrar), 1, "http") for registrar in _ROUTE_REGISTRARS),
|
|
119
|
+
# ``Model.objects.raw(sql)`` for any model class: the statement is the first
|
|
120
|
+
# argument, its parameters are not.
|
|
121
|
+
SuffixSink("objects.raw", TaintKind.SQL, ((TaintKind.SQL, (0,)),)),
|
|
122
|
+
SuffixSink("objects.extra", TaintKind.SQL),
|
|
123
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""FastAPI security models: route handlers and response sinks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.plugins import ModelPlugin
|
|
8
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
9
|
+
from coretrace_python.taint import EntryPoint, Model, RouteRegistrar, Sink, TaintKind
|
|
10
|
+
|
|
11
|
+
_METHODS = ("get", "post", "put", "delete", "patch", "options", "head", "api_route", "websocket")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _sym(path: str) -> SymbolId:
|
|
15
|
+
return SymbolId(f"python.{path}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FastApiModels(ModelPlugin):
|
|
19
|
+
name: ClassVar[str] = "fastapi-models"
|
|
20
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
21
|
+
*(EntryPoint(_sym(f"fastapi.FastAPI.{method}"), "http") for method in _METHODS),
|
|
22
|
+
*(EntryPoint(_sym(f"fastapi.APIRouter.{method}"), "http") for method in _METHODS),
|
|
23
|
+
RouteRegistrar(_sym("fastapi.FastAPI.add_api_route"), 1, "http"),
|
|
24
|
+
RouteRegistrar(_sym("fastapi.APIRouter.add_api_route"), 1, "http"),
|
|
25
|
+
Sink(_sym("fastapi.responses.HTMLResponse"), TaintKind.HTML),
|
|
26
|
+
Sink(_sym("fastapi.responses.FileResponse"), TaintKind.PATH),
|
|
27
|
+
Sink(_sym("fastapi.responses.RedirectResponse"), TaintKind.REDIRECT),
|
|
28
|
+
Sink(_sym("starlette.responses.RedirectResponse"), TaintKind.REDIRECT),
|
|
29
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Flask security models: HTTP sources, route handlers, HTML sinks and escaping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from coretrace_python.plugins import ModelPlugin
|
|
8
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
9
|
+
from coretrace_python.taint import (
|
|
10
|
+
AuthorizationGuard,
|
|
11
|
+
EntryPoint,
|
|
12
|
+
Model,
|
|
13
|
+
RouteRegistrar,
|
|
14
|
+
Sanitizer,
|
|
15
|
+
Sink,
|
|
16
|
+
Source,
|
|
17
|
+
TaintKind,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_REQUEST_ATTRIBUTES = (
|
|
21
|
+
"args", "form", "values", "json", "data", "cookies", "headers", "files",
|
|
22
|
+
"get_json", "get_data", "url", "full_path", "path", "query_string", "stream",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sym(path: str) -> SymbolId:
|
|
27
|
+
return SymbolId(f"python.{path}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FlaskModels(ModelPlugin):
|
|
31
|
+
name: ClassVar[str] = "flask-models"
|
|
32
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
33
|
+
*(Source(_sym(f"flask.request.{attribute}"), "http") for attribute in _REQUEST_ATTRIBUTES),
|
|
34
|
+
EntryPoint(_sym("flask.Flask.route"), "http"),
|
|
35
|
+
EntryPoint(_sym("flask.Blueprint.route"), "http"),
|
|
36
|
+
RouteRegistrar(_sym("flask.Flask.add_url_rule"), 2, "http", keyword="view_func"),
|
|
37
|
+
RouteRegistrar(_sym("flask.Blueprint.add_url_rule"), 2, "http", keyword="view_func"),
|
|
38
|
+
Sink(_sym("flask.render_template_string"), TaintKind.HTML),
|
|
39
|
+
Sink(_sym("flask.make_response"), TaintKind.HTML),
|
|
40
|
+
Sink(_sym("flask.Response"), TaintKind.HTML),
|
|
41
|
+
Sink(_sym("flask.Markup"), TaintKind.HTML),
|
|
42
|
+
Sink(_sym("flask.send_file"), TaintKind.PATH),
|
|
43
|
+
Sink(_sym("flask.redirect"), TaintKind.REDIRECT),
|
|
44
|
+
Sink(_sym("werkzeug.utils.redirect"), TaintKind.REDIRECT),
|
|
45
|
+
Sink(_sym("flask.request.files.save"), TaintKind.PATH),
|
|
46
|
+
Sanitizer(_sym("werkzeug.utils.secure_filename"), TaintKind.PATH),
|
|
47
|
+
Sanitizer(_sym("flask.escape"), TaintKind.HTML),
|
|
48
|
+
Sanitizer(_sym("markupsafe.escape"), TaintKind.HTML),
|
|
49
|
+
AuthorizationGuard(_sym("flask_login.login_required"), "login"),
|
|
50
|
+
AuthorizationGuard(_sym("flask_login.fresh_login_required"), "login"),
|
|
51
|
+
AuthorizationGuard(_sym("flask_login.current_user.is_authenticated"), "login"),
|
|
52
|
+
AuthorizationGuard(_sym("flask.session"), "session"),
|
|
53
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Requests and httpx models: every request function is a SSRF sink, and what it returns
|
|
2
|
+
is an ``http-response`` source, so data fetched from a remote server is untrusted."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.plugins import ModelPlugin
|
|
9
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
10
|
+
from coretrace_python.taint import Model, Sink, Source, TaintKind
|
|
11
|
+
|
|
12
|
+
_METHODS = ("get", "post", "put", "patch", "delete", "head", "options", "request")
|
|
13
|
+
|
|
14
|
+
_CALLERS = (
|
|
15
|
+
"requests",
|
|
16
|
+
"requests.Session",
|
|
17
|
+
"requests.api",
|
|
18
|
+
"httpx",
|
|
19
|
+
"httpx.Client",
|
|
20
|
+
"httpx.AsyncClient",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_REQUEST_FUNCTIONS = (
|
|
24
|
+
*(f"{caller}.{method}" for caller in _CALLERS for method in _METHODS),
|
|
25
|
+
"requests.Session.send",
|
|
26
|
+
"httpx.stream",
|
|
27
|
+
"httpx.Client.stream",
|
|
28
|
+
"httpx.AsyncClient.stream",
|
|
29
|
+
"httpx.Client.send",
|
|
30
|
+
"httpx.AsyncClient.send",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sym(path: str) -> SymbolId:
|
|
35
|
+
return SymbolId(f"python.{path}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HttpClientModels(ModelPlugin):
|
|
39
|
+
name: ClassVar[str] = "http-client-models"
|
|
40
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
41
|
+
*(Sink(_sym(function), TaintKind.SSRF) for function in _REQUEST_FUNCTIONS),
|
|
42
|
+
*(Source(_sym(function), "http-response") for function in _REQUEST_FUNCTIONS),
|
|
43
|
+
)
|