coretrace-python-analyzer 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coretrace_python/__init__.py +1 -1
- coretrace_python/bundled/models/aiohttp/aiohttp_models.py +61 -0
- coretrace_python/bundled/models/aiohttp/plugin.toml +9 -0
- coretrace_python/bundled/models/bottle/bottle_models.py +39 -0
- coretrace_python/bundled/models/bottle/plugin.toml +9 -0
- coretrace_python/bundled/models/db_drivers/db_driver_models.py +71 -0
- coretrace_python/bundled/models/db_drivers/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +11 -3
- coretrace_python/bundled/models/flask/flask_models.py +6 -2
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +12 -6
- coretrace_python/bundled/models/tornado/plugin.toml +9 -0
- coretrace_python/bundled/models/tornado/tornado_models.py +37 -0
- coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +25 -0
- coretrace_python/bundled/syntax/insecure_tempfile/plugin.toml +9 -0
- coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +40 -0
- coretrace_python/bundled/syntax/insecure_tls/plugin.toml +9 -0
- coretrace_python/bundled/syntax/unsafe_archive/plugin.toml +9 -0
- coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +36 -0
- coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +9 -0
- coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +35 -0
- coretrace_python/bundled/syntax/weak_random/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_random/weak_random.py +86 -0
- coretrace_python/cache.py +14 -1
- coretrace_python/cfg/builder.py +7 -2
- coretrace_python/cli.py +40 -2
- coretrace_python/engine.py +48 -6
- coretrace_python/findings/baseline.py +99 -0
- coretrace_python/findings/model.py +9 -0
- coretrace_python/findings/suppressions.py +71 -0
- coretrace_python/frontend/ast_adapter.py +17 -12
- coretrace_python/hir/nodes.py +17 -1
- coretrace_python/interprocedural/__init__.py +2 -0
- coretrace_python/interprocedural/callgraph.py +56 -9
- coretrace_python/interprocedural/summaries.py +65 -4
- coretrace_python/ir/lowering.py +78 -9
- coretrace_python/ir/model.py +24 -0
- coretrace_python/ir/printer.py +6 -0
- coretrace_python/plugins/detectors.py +23 -10
- coretrace_python/plugins/secrets.py +60 -4
- coretrace_python/reporters/json_format.py +17 -5
- coretrace_python/reporters/report.py +24 -1
- coretrace_python/reporters/sarif.py +40 -16
- coretrace_python/reporters/text.py +5 -1
- coretrace_python/semantic/scopes.py +12 -1
- coretrace_python/taint/engine.py +57 -12
- coretrace_python/taint/routes.py +54 -3
- {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/METADATA +7 -3
- {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/RECORD +52 -30
- coretrace_python_analyzer-0.2.0.dist-info/licenses/LICENSE +202 -0
- coretrace_python_analyzer-0.2.0.dist-info/licenses/NOTICE +4 -0
- {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/entry_points.txt +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""aiohttp security models: handlers receive HTTP input whether they take an annotated
|
|
2
|
+
``web.Request``, are registered on the router or decorated by a ``RouteTableDef``;
|
|
3
|
+
responses, file responses and redirect exceptions are sinks; client sessions are SSRF
|
|
4
|
+
sinks whose responses are untrusted."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from coretrace_python.plugins import ModelPlugin
|
|
11
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
12
|
+
from coretrace_python.taint import (
|
|
13
|
+
EntryPoint,
|
|
14
|
+
Model,
|
|
15
|
+
RouteRegistrar,
|
|
16
|
+
Sink,
|
|
17
|
+
Source,
|
|
18
|
+
TaintKind,
|
|
19
|
+
TypedParameter,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_METHODS = ("get", "post", "put", "patch", "delete", "head", "options")
|
|
23
|
+
_REQUEST_CLASSES = ("aiohttp.web.Request", "aiohttp.web_request.Request")
|
|
24
|
+
_ROUTERS = ("aiohttp.web.Application.router", "aiohttp.web.UrlDispatcher")
|
|
25
|
+
_REDIRECTS = ("HTTPFound", "HTTPMovedPermanently", "HTTPSeeOther", "HTTPTemporaryRedirect", "HTTPPermanentRedirect")
|
|
26
|
+
_CLIENT_FUNCTIONS = (
|
|
27
|
+
*(f"aiohttp.ClientSession.{method}" for method in (*_METHODS, "request")),
|
|
28
|
+
*(f"aiohttp.client.ClientSession.{method}" for method in (*_METHODS, "request")),
|
|
29
|
+
"aiohttp.request",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sym(path: str) -> SymbolId:
|
|
34
|
+
return SymbolId(f"python.{path}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AiohttpModels(ModelPlugin):
|
|
38
|
+
name: ClassVar[str] = "aiohttp-models"
|
|
39
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
40
|
+
# The whole request is attacker-controlled: its query, match info, headers,
|
|
41
|
+
# cookies and body all come from the client.
|
|
42
|
+
*(TypedParameter(_sym(cls), "http") for cls in _REQUEST_CLASSES),
|
|
43
|
+
# ``@routes.get('/')`` on a ``RouteTableDef``.
|
|
44
|
+
*(EntryPoint(_sym(f"aiohttp.web.RouteTableDef.{method}"), "http") for method in (*_METHODS, "route", "view")),
|
|
45
|
+
# ``app.router.add_route('GET', '/', handler)`` and ``app.router.add_get('/', handler)``.
|
|
46
|
+
*(RouteRegistrar(_sym(f"{router}.add_route"), 2, "http", keyword="handler") for router in _ROUTERS),
|
|
47
|
+
*(
|
|
48
|
+
RouteRegistrar(_sym(f"{router}.add_{method}"), 1, "http", keyword="handler")
|
|
49
|
+
for router in _ROUTERS
|
|
50
|
+
for method in (*_METHODS, "view")
|
|
51
|
+
),
|
|
52
|
+
# ``app.add_routes([web.get('/', handler)])``: the route definitions name the handler.
|
|
53
|
+
RouteRegistrar(_sym("aiohttp.web.route"), 2, "http", keyword="handler"),
|
|
54
|
+
*(RouteRegistrar(_sym(f"aiohttp.web.{method}"), 1, "http", keyword="handler") for method in (*_METHODS, "view")),
|
|
55
|
+
Sink(_sym("aiohttp.web.Response"), TaintKind.HTML),
|
|
56
|
+
Sink(_sym("aiohttp.web.FileResponse"), TaintKind.PATH),
|
|
57
|
+
*(Sink(_sym(f"aiohttp.web.{exception}"), TaintKind.REDIRECT) for exception in _REDIRECTS),
|
|
58
|
+
*(Sink(_sym(f"aiohttp.web_exceptions.{exception}"), TaintKind.REDIRECT) for exception in _REDIRECTS),
|
|
59
|
+
*(Sink(_sym(function), TaintKind.SSRF) for function in _CLIENT_FUNCTIONS),
|
|
60
|
+
*(Source(_sym(function), "http-response") for function in _CLIENT_FUNCTIONS),
|
|
61
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Bottle security models: ``@route``, ``@get`` and the methods of a ``Bottle`` application
|
|
2
|
+
are entry points, ``bottle.request`` is the HTTP source, ``redirect``, ``static_file``,
|
|
3
|
+
``template`` and ``HTTPResponse`` are sinks, ``html_escape`` a sanitizer."""
|
|
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 EntryPoint, Model, Sanitizer, Sink, Source, TaintKind
|
|
12
|
+
|
|
13
|
+
_METHODS = ("route", "get", "post", "put", "delete", "patch")
|
|
14
|
+
_REQUEST_ATTRIBUTES = (
|
|
15
|
+
"query", "forms", "params", "json", "body", "headers", "cookies", "GET", "POST", "files",
|
|
16
|
+
"url", "path", "fullpath", "query_string", "get_cookie", "get_header",
|
|
17
|
+
)
|
|
18
|
+
_FIRST_ONLY = lambda kind: ((kind, (0,)),)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _sym(path: str) -> SymbolId:
|
|
22
|
+
return SymbolId(f"python.{path}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BottleModels(ModelPlugin):
|
|
26
|
+
name: ClassVar[str] = "bottle-models"
|
|
27
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
28
|
+
*(EntryPoint(_sym(f"bottle.{method}"), "http") for method in _METHODS),
|
|
29
|
+
*(EntryPoint(_sym(f"bottle.Bottle.{method}"), "http") for method in _METHODS),
|
|
30
|
+
*(Source(_sym(f"bottle.request.{attribute}"), "http") for attribute in _REQUEST_ATTRIBUTES),
|
|
31
|
+
Sink(_sym("bottle.redirect"), TaintKind.REDIRECT, _FIRST_ONLY(TaintKind.REDIRECT)),
|
|
32
|
+
# ``static_file(filename, root)``: the root is the application's own.
|
|
33
|
+
Sink(_sym("bottle.static_file"), TaintKind.PATH, _FIRST_ONLY(TaintKind.PATH)),
|
|
34
|
+
# ``template(source_or_name, **variables)``: a tainted template is injection, the
|
|
35
|
+
# variables are escaped by the engine.
|
|
36
|
+
Sink(_sym("bottle.template"), TaintKind.HTML, _FIRST_ONLY(TaintKind.HTML)),
|
|
37
|
+
Sink(_sym("bottle.HTTPResponse"), TaintKind.HTML),
|
|
38
|
+
Sanitizer(_sym("bottle.html_escape"), TaintKind.HTML),
|
|
39
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Database driver models: the statement argument of aiopg, asyncpg, psycopg2 and PyMySQL
|
|
2
|
+
query methods is a SQL sink; the parameter tuple or mapping is not a statement."""
|
|
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, TaintKind
|
|
11
|
+
|
|
12
|
+
_STATEMENT_ONLY = ((TaintKind.SQL, (0,)),)
|
|
13
|
+
_CURSOR_METHODS = ("execute", "executemany", "mogrify")
|
|
14
|
+
_ASYNCPG_METHODS = ("execute", "executemany", "fetch", "fetchrow", "fetchval", "cursor")
|
|
15
|
+
|
|
16
|
+
# Every way a cursor is reached: through a connection object, an annotated class or a
|
|
17
|
+
# pool; the symbol derivation follows call chains, so ``connect().cursor().execute`` is
|
|
18
|
+
# ``connect.cursor.execute``.
|
|
19
|
+
_CURSORS = (
|
|
20
|
+
"aiopg.connect.cursor",
|
|
21
|
+
"aiopg.Connection.cursor",
|
|
22
|
+
"aiopg.connection.Connection.cursor",
|
|
23
|
+
"aiopg.Cursor",
|
|
24
|
+
"aiopg.cursor.Cursor",
|
|
25
|
+
"aiopg.create_pool.acquire.cursor",
|
|
26
|
+
"aiopg.Pool.acquire.cursor",
|
|
27
|
+
"aiopg.pool.Pool.acquire.cursor",
|
|
28
|
+
"aiopg.create_pool.cursor",
|
|
29
|
+
"aiopg.Pool.cursor",
|
|
30
|
+
"psycopg2.connect.cursor",
|
|
31
|
+
"psycopg2.extensions.connection.cursor",
|
|
32
|
+
"psycopg2.extensions.cursor",
|
|
33
|
+
"psycopg2.extras.DictCursor",
|
|
34
|
+
"psycopg2.extras.RealDictCursor",
|
|
35
|
+
"pymysql.connect.cursor",
|
|
36
|
+
"pymysql.connections.Connection.cursor",
|
|
37
|
+
"pymysql.Connection.cursor",
|
|
38
|
+
"pymysql.cursors.Cursor",
|
|
39
|
+
"pymysql.cursors.DictCursor",
|
|
40
|
+
)
|
|
41
|
+
_ASYNCPG_CONNECTIONS = (
|
|
42
|
+
"asyncpg.connect",
|
|
43
|
+
"asyncpg.Connection",
|
|
44
|
+
"asyncpg.connection.Connection",
|
|
45
|
+
"asyncpg.create_pool",
|
|
46
|
+
"asyncpg.Pool",
|
|
47
|
+
"asyncpg.pool.Pool",
|
|
48
|
+
"asyncpg.create_pool.acquire",
|
|
49
|
+
"asyncpg.Pool.acquire",
|
|
50
|
+
"asyncpg.pool.Pool.acquire",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sym(path: str) -> SymbolId:
|
|
55
|
+
return SymbolId(f"python.{path}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DbDriverModels(ModelPlugin):
|
|
59
|
+
name: ClassVar[str] = "db-driver-models"
|
|
60
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
61
|
+
*(
|
|
62
|
+
Sink(_sym(f"{cursor}.{method}"), TaintKind.SQL, _STATEMENT_ONLY)
|
|
63
|
+
for cursor in _CURSORS
|
|
64
|
+
for method in _CURSOR_METHODS
|
|
65
|
+
),
|
|
66
|
+
*(
|
|
67
|
+
Sink(_sym(f"{connection}.{method}"), TaintKind.SQL, _STATEMENT_ONLY)
|
|
68
|
+
for connection in _ASYNCPG_CONNECTIONS
|
|
69
|
+
for method in _ASYNCPG_METHODS
|
|
70
|
+
),
|
|
71
|
+
)
|
|
@@ -20,6 +20,7 @@ from coretrace_python.taint import (
|
|
|
20
20
|
RouteRegistrar,
|
|
21
21
|
Sanitizer,
|
|
22
22
|
Sink,
|
|
23
|
+
Source,
|
|
23
24
|
SuffixSink,
|
|
24
25
|
TaintKind,
|
|
25
26
|
TypedParameter,
|
|
@@ -86,6 +87,9 @@ _AUTHORIZATION_DECORATORS = (
|
|
|
86
87
|
)
|
|
87
88
|
|
|
88
89
|
|
|
90
|
+
_TARGET_ONLY = ((TaintKind.REDIRECT, (0,)),)
|
|
91
|
+
|
|
92
|
+
|
|
89
93
|
def _sym(path: str) -> SymbolId:
|
|
90
94
|
return SymbolId(f"python.{path}")
|
|
91
95
|
|
|
@@ -95,6 +99,8 @@ class DjangoModels(ModelPlugin):
|
|
|
95
99
|
models: ClassVar[tuple[Model, ...]] = (
|
|
96
100
|
*(TypedParameter(_sym(cls), "http") for cls in _REQUEST_CLASSES),
|
|
97
101
|
*(EntryPoint(_sym(base), "http") for base in _VIEW_BASES),
|
|
102
|
+
# ``self.request`` in a class-based view: the attribute is inherited from the base.
|
|
103
|
+
*(Source(_sym(f"{base}.request"), "http") for base in _VIEW_BASES),
|
|
98
104
|
*(EntryPoint(_sym(decorator), "http") for decorator in _VIEW_DECORATORS),
|
|
99
105
|
Sink(_sym("django.db.connection.cursor.execute"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),)),
|
|
100
106
|
Sink(_sym("django.db.connection.cursor.executemany"), TaintKind.SQL | TaintKind.CREDENTIAL, ((TaintKind.SQL, (0,)),)),
|
|
@@ -107,9 +113,11 @@ class DjangoModels(ModelPlugin):
|
|
|
107
113
|
Sink(_sym("django.http.response.HttpResponse"), TaintKind.HTML),
|
|
108
114
|
Sink(_sym("django.template.Template"), TaintKind.HTML),
|
|
109
115
|
Sink(_sym("django.http.FileResponse"), TaintKind.PATH),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Sink(_sym("django.
|
|
116
|
+
# Only the target is a redirect: the other arguments of ``redirect`` are route
|
|
117
|
+
# parameters resolved through the URL configuration.
|
|
118
|
+
Sink(_sym("django.shortcuts.redirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
119
|
+
Sink(_sym("django.http.HttpResponseRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
120
|
+
Sink(_sym("django.http.HttpResponsePermanentRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
113
121
|
Sanitizer(_sym("django.utils.html.escape"), TaintKind.HTML),
|
|
114
122
|
Sanitizer(_sym("django.utils.html.conditional_escape"), TaintKind.HTML),
|
|
115
123
|
*(AuthorizationGuard(_sym(decorator), label) for decorator, label in _AUTHORIZATION_DECORATORS),
|
|
@@ -23,6 +23,9 @@ _REQUEST_ATTRIBUTES = (
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
_TARGET_ONLY = ((TaintKind.REDIRECT, (0,)),)
|
|
27
|
+
|
|
28
|
+
|
|
26
29
|
def _sym(path: str) -> SymbolId:
|
|
27
30
|
return SymbolId(f"python.{path}")
|
|
28
31
|
|
|
@@ -40,8 +43,9 @@ class FlaskModels(ModelPlugin):
|
|
|
40
43
|
Sink(_sym("flask.Response"), TaintKind.HTML),
|
|
41
44
|
Sink(_sym("flask.Markup"), TaintKind.HTML),
|
|
42
45
|
Sink(_sym("flask.send_file"), TaintKind.PATH),
|
|
43
|
-
|
|
44
|
-
Sink(_sym("
|
|
46
|
+
# ``redirect(location, code)``: the status code is not a target.
|
|
47
|
+
Sink(_sym("flask.redirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
48
|
+
Sink(_sym("werkzeug.utils.redirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
45
49
|
Sink(_sym("flask.request.files.save"), TaintKind.PATH),
|
|
46
50
|
Sanitizer(_sym("werkzeug.utils.secure_filename"), TaintKind.PATH),
|
|
47
51
|
Sanitizer(_sym("flask.escape"), TaintKind.HTML),
|
|
@@ -8,6 +8,9 @@ from coretrace_python.plugins import ModelPlugin
|
|
|
8
8
|
from coretrace_python.semantic.symbols import SymbolId
|
|
9
9
|
from coretrace_python.taint import Model, Sanitizer, Sink, Source, TaintKind, Validator
|
|
10
10
|
|
|
11
|
+
_ENVIRONMENT_KINDS = TaintKind.ALL & ~(TaintKind.COMMAND | TaintKind.PATH)
|
|
12
|
+
_PROCESS_OUTPUT_KINDS = TaintKind.ALL & ~TaintKind.PATH
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
def _sym(path: str) -> SymbolId:
|
|
13
16
|
return SymbolId(f"python.{path}")
|
|
@@ -18,13 +21,16 @@ class PythonStdlibModels(ModelPlugin):
|
|
|
18
21
|
models: ClassVar[tuple[Model, ...]] = (
|
|
19
22
|
Source(_sym("builtins.input"), "stdin"),
|
|
20
23
|
Source(_sym("sys.stdin"), "stdin"),
|
|
21
|
-
# A command-line tool is expected to open the paths
|
|
24
|
+
# Operator-controlled inputs. A command-line tool is expected to open the paths
|
|
25
|
+
# it is given; the environment is set by whoever runs the program, so a command
|
|
26
|
+
# or a path built from it is not an injection; the output of a local process is
|
|
27
|
+
# not a path either, but a downloaded script piped into a shell is a real flaw.
|
|
22
28
|
Source(_sym("sys.argv"), "argv", TaintKind.ALL & ~TaintKind.PATH),
|
|
23
|
-
Source(_sym("os.environ"), "environment"),
|
|
24
|
-
Source(_sym("subprocess.run.stdout"), "process-output"),
|
|
25
|
-
Source(_sym("subprocess.check_output"), "process-output"),
|
|
26
|
-
Source(_sym("subprocess.getoutput"), "process-output"),
|
|
27
|
-
Source(_sym("subprocess.Popen.communicate"), "process-output"),
|
|
29
|
+
Source(_sym("os.environ"), "environment", _ENVIRONMENT_KINDS),
|
|
30
|
+
Source(_sym("subprocess.run.stdout"), "process-output", _PROCESS_OUTPUT_KINDS),
|
|
31
|
+
Source(_sym("subprocess.check_output"), "process-output", _PROCESS_OUTPUT_KINDS),
|
|
32
|
+
Source(_sym("subprocess.getoutput"), "process-output", _PROCESS_OUTPUT_KINDS),
|
|
33
|
+
Source(_sym("subprocess.Popen.communicate"), "process-output", _PROCESS_OUTPUT_KINDS),
|
|
28
34
|
Sink(_sym("os.system"), TaintKind.COMMAND),
|
|
29
35
|
Sink(_sym("os.popen"), TaintKind.COMMAND),
|
|
30
36
|
Sink(_sym("subprocess.run"), TaintKind.COMMAND),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Tornado security models: ``RequestHandler`` subclasses are entry points whose inherited
|
|
2
|
+
request methods are HTTP sources and whose ``write``, ``finish`` and ``redirect`` are
|
|
3
|
+
sinks; the HTTP clients are SSRF sinks with untrusted responses."""
|
|
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 EntryPoint, Model, Sanitizer, Sink, Source, TaintKind
|
|
12
|
+
|
|
13
|
+
_HANDLERS = ("tornado.web.RequestHandler", "tornado.websocket.WebSocketHandler")
|
|
14
|
+
_REQUEST_METHODS = (
|
|
15
|
+
"get_argument", "get_arguments", "get_query_argument", "get_query_arguments",
|
|
16
|
+
"get_body_argument", "get_body_arguments", "get_cookie", "request", "path_args", "path_kwargs",
|
|
17
|
+
)
|
|
18
|
+
_CLIENTS = ("tornado.httpclient.AsyncHTTPClient.fetch", "tornado.httpclient.HTTPClient.fetch")
|
|
19
|
+
_TARGET_ONLY = ((TaintKind.REDIRECT, (0,)),)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sym(path: str) -> SymbolId:
|
|
23
|
+
return SymbolId(f"python.{path}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TornadoModels(ModelPlugin):
|
|
27
|
+
name: ClassVar[str] = "tornado-models"
|
|
28
|
+
models: ClassVar[tuple[Model, ...]] = (
|
|
29
|
+
*(EntryPoint(_sym(handler), "http") for handler in _HANDLERS),
|
|
30
|
+
*(Source(_sym(f"{handler}.{method}"), "http") for handler in _HANDLERS for method in _REQUEST_METHODS),
|
|
31
|
+
*(Sink(_sym(f"{handler}.write"), TaintKind.HTML) for handler in _HANDLERS),
|
|
32
|
+
*(Sink(_sym(f"{handler}.finish"), TaintKind.HTML) for handler in _HANDLERS),
|
|
33
|
+
*(Sink(_sym(f"{handler}.redirect"), TaintKind.REDIRECT, _TARGET_ONLY) for handler in _HANDLERS),
|
|
34
|
+
*(Sink(_sym(client), TaintKind.SSRF) for client in _CLIENTS),
|
|
35
|
+
*(Source(_sym(client), "http-response") for client in _CLIENTS),
|
|
36
|
+
Sanitizer(_sym("tornado.escape.xhtml_escape"), TaintKind.HTML),
|
|
37
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""``tempfile.mktemp`` returns a name without creating the file: another process can
|
|
2
|
+
create it first. ``mkstemp``, ``NamedTemporaryFile`` and ``TemporaryDirectory`` are safe."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
+
from coretrace_python.findings import Severity
|
|
10
|
+
from coretrace_python.interprocedural import CallGraphAnalysis
|
|
11
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
12
|
+
from coretrace_python.plugins import SymbolCallDetector
|
|
13
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InsecureTempFilePlugin(SymbolCallDetector):
|
|
17
|
+
name: ClassVar[str] = "insecure-temp-file"
|
|
18
|
+
rule_id: ClassVar[str] = "insecure-temp-file"
|
|
19
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
|
|
20
|
+
symbols: ClassVar[frozenset[SymbolId]] = frozenset({SymbolId("python.tempfile.mktemp")})
|
|
21
|
+
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
22
|
+
message_template: ClassVar[str] = (
|
|
23
|
+
"call to {symbol} names a temporary file without creating it; use mkstemp or "
|
|
24
|
+
"NamedTemporaryFile"
|
|
25
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Disabled certificate verification: ``verify=False`` on an HTTP client call, or an
|
|
2
|
+
``ssl`` context built without verification, lets any peer impersonate the server."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
10
|
+
from coretrace_python.findings import Severity
|
|
11
|
+
from coretrace_python.interprocedural import CallGraphAnalysis
|
|
12
|
+
from coretrace_python.ir.model import Call, Constant, Instruction, Value
|
|
13
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
14
|
+
from coretrace_python.plugins import SymbolCallDetector
|
|
15
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
16
|
+
|
|
17
|
+
_METHODS = ("get", "post", "put", "patch", "delete", "head", "options", "request")
|
|
18
|
+
_CALLERS = ("requests", "requests.Session", "requests.api", "httpx", "httpx.Client", "httpx.AsyncClient")
|
|
19
|
+
HTTP_CALLS = frozenset(
|
|
20
|
+
SymbolId(f"python.{caller}.{method}") for caller in _CALLERS for method in _METHODS
|
|
21
|
+
) | frozenset(SymbolId(f"python.{client}") for client in ("httpx.Client", "httpx.AsyncClient"))
|
|
22
|
+
UNVERIFIED = frozenset({SymbolId("python.ssl._create_unverified_context")})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InsecureTlsPlugin(SymbolCallDetector):
|
|
26
|
+
name: ClassVar[str] = "insecure-tls"
|
|
27
|
+
rule_id: ClassVar[str] = "insecure-tls"
|
|
28
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
|
|
29
|
+
symbols: ClassVar[frozenset[SymbolId]] = HTTP_CALLS | UNVERIFIED
|
|
30
|
+
severity: ClassVar[Severity] = Severity.HIGH
|
|
31
|
+
message_template: ClassVar[str] = "call to {symbol} disables certificate verification"
|
|
32
|
+
|
|
33
|
+
def accepts(self, call: Call, symbol: SymbolId, defs: Mapping[Value, Instruction]) -> bool:
|
|
34
|
+
if symbol in UNVERIFIED:
|
|
35
|
+
return True
|
|
36
|
+
for name, value in call.keywords:
|
|
37
|
+
if name == "verify":
|
|
38
|
+
defined = defs.get(value)
|
|
39
|
+
return isinstance(defined, Constant) and defined.value is False
|
|
40
|
+
return False
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
name = "unsafe-archive-extraction"
|
|
2
|
+
version = "1.0.0"
|
|
3
|
+
plugin_api = ">=1,<2"
|
|
4
|
+
requires = ["interprocedural.callgraph", "ir.ssa"]
|
|
5
|
+
provides = ["vulnerability.unsafe-archive-extraction"]
|
|
6
|
+
|
|
7
|
+
[entrypoint]
|
|
8
|
+
module = "unsafe_archive"
|
|
9
|
+
class = "UnsafeArchivePlugin"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Archive extraction without member checks: a crafted archive with ``../`` members or
|
|
2
|
+
absolute paths writes outside the target directory. ``tarfile`` accepts a ``filter``."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
10
|
+
from coretrace_python.findings import Severity
|
|
11
|
+
from coretrace_python.interprocedural import CallGraphAnalysis
|
|
12
|
+
from coretrace_python.ir.model import Call, Instruction, Value
|
|
13
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
14
|
+
from coretrace_python.plugins import SymbolCallDetector
|
|
15
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
16
|
+
|
|
17
|
+
_OPENERS = ("tarfile.open", "tarfile.TarFile", "tarfile.TarFile.open", "zipfile.ZipFile", "zipfile.Path")
|
|
18
|
+
_EXTRACTORS = tuple(f"{opener}.{method}" for opener in _OPENERS for method in ("extractall", "extract"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnsafeArchivePlugin(SymbolCallDetector):
|
|
22
|
+
name: ClassVar[str] = "unsafe-archive-extraction"
|
|
23
|
+
rule_id: ClassVar[str] = "unsafe-archive-extraction"
|
|
24
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
|
|
25
|
+
symbols: ClassVar[frozenset[SymbolId]] = frozenset(
|
|
26
|
+
SymbolId(f"python.{p}") for p in (*_EXTRACTORS, "shutil.unpack_archive")
|
|
27
|
+
)
|
|
28
|
+
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
29
|
+
message_template: ClassVar[str] = (
|
|
30
|
+
"call to {symbol} extracts an archive without checking its member paths; a "
|
|
31
|
+
"crafted archive writes outside the target directory"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def accepts(self, call: Call, symbol: SymbolId, defs: Mapping[Value, Instruction]) -> bool:
|
|
35
|
+
# ``extractall(path, filter="data")`` rejects unsafe members (Python 3.12).
|
|
36
|
+
return not any(name == "filter" for name, _ in call.keywords)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""XML parsers of the standard library and lxml expand entities: a crafted document reads
|
|
2
|
+
local files or exhausts memory. ``defusedxml`` offers the same functions safely."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
+
from coretrace_python.findings import Severity
|
|
10
|
+
from coretrace_python.interprocedural import CallGraphAnalysis
|
|
11
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
12
|
+
from coretrace_python.plugins import SymbolCallDetector
|
|
13
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
14
|
+
|
|
15
|
+
_PARSERS = (
|
|
16
|
+
"xml.etree.ElementTree.parse", "xml.etree.ElementTree.fromstring", "xml.etree.ElementTree.iterparse",
|
|
17
|
+
"xml.etree.ElementTree.XMLParser", "xml.etree.ElementTree.fromstringlist",
|
|
18
|
+
"xml.dom.minidom.parse", "xml.dom.minidom.parseString",
|
|
19
|
+
"xml.dom.pulldom.parse", "xml.dom.pulldom.parseString",
|
|
20
|
+
"xml.sax.parse", "xml.sax.parseString", "xml.sax.make_parser",
|
|
21
|
+
"xml.dom.expatbuilder.parse", "xml.dom.expatbuilder.parseString",
|
|
22
|
+
"lxml.etree.parse", "lxml.etree.fromstring", "lxml.etree.XMLParser", "lxml.etree.iterparse",
|
|
23
|
+
"lxml.etree.XML",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnsafeXmlPlugin(SymbolCallDetector):
|
|
28
|
+
name: ClassVar[str] = "unsafe-xml"
|
|
29
|
+
rule_id: ClassVar[str] = "unsafe-xml"
|
|
30
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CallGraphAnalysis})
|
|
31
|
+
symbols: ClassVar[frozenset[SymbolId]] = frozenset(SymbolId(f"python.{p}") for p in _PARSERS)
|
|
32
|
+
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
33
|
+
message_template: ClassVar[str] = (
|
|
34
|
+
"call to {symbol} parses XML with entity expansion enabled; use defusedxml"
|
|
35
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``random`` builds a secret: the Mersenne Twister is predictable from a few outputs, so
|
|
2
|
+
a token, key or session identifier drawn from it can be guessed. ``secrets`` and
|
|
3
|
+
``random.SystemRandom`` are the alternatives. A call is reported when its result is
|
|
4
|
+
assigned to a credential-like name, or when the enclosing function's name says it
|
|
5
|
+
produces one."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Iterator, Sequence
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
14
|
+
from coretrace_python.findings import Confidence, Finding, Severity
|
|
15
|
+
from coretrace_python.hir import nodes
|
|
16
|
+
from coretrace_python.hir.visitors import Node, children
|
|
17
|
+
from coretrace_python.plugins import Plugin, PluginContext
|
|
18
|
+
from coretrace_python.semantic.scopes import ScopeAnalysis
|
|
19
|
+
from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId
|
|
20
|
+
|
|
21
|
+
_FUNCTIONS = ("random", "randint", "choice", "choices", "randrange", "getrandbits", "uniform", "sample", "randbytes")
|
|
22
|
+
RANDOM = frozenset(SymbolId(f"python.random.{f}") for f in _FUNCTIONS)
|
|
23
|
+
CREDENTIAL = re.compile(r"(?i)(token|secret|password|passwd|api_?key|session|nonce|salt|otp|csrf|auth)")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WeakRandomPlugin(Plugin):
|
|
27
|
+
name: ClassVar[str] = "weak-random"
|
|
28
|
+
rule_id: ClassVar[str] = "weak-random"
|
|
29
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({ScopeAnalysis, SymbolAnalysis})
|
|
30
|
+
|
|
31
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
32
|
+
scopes = ctx.get(ScopeAnalysis)
|
|
33
|
+
symbols = ctx.get(SymbolAnalysis)
|
|
34
|
+
findings: list[Finding] = []
|
|
35
|
+
for function in ctx.functions():
|
|
36
|
+
scope = scopes.scope_for(function).id
|
|
37
|
+
for target, call in _random_calls(function, scope, symbols):
|
|
38
|
+
purpose = target if target is not None and CREDENTIAL.search(target) else None
|
|
39
|
+
if purpose is None and CREDENTIAL.search(function.name):
|
|
40
|
+
purpose = function.name
|
|
41
|
+
if purpose is None:
|
|
42
|
+
continue
|
|
43
|
+
symbol = symbols.resolve_expression(scope, call.callee)
|
|
44
|
+
findings.append(
|
|
45
|
+
Finding(
|
|
46
|
+
self.rule_id,
|
|
47
|
+
f"{symbol} is not a secure random source; {purpose!r} looks like a secret",
|
|
48
|
+
Severity.MEDIUM,
|
|
49
|
+
Confidence.MEDIUM,
|
|
50
|
+
call.span,
|
|
51
|
+
function.name,
|
|
52
|
+
{"symbol": str(symbol), "purpose": purpose},
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return findings
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _random_calls(
|
|
59
|
+
function: nodes.Function, scope: object, symbols: object
|
|
60
|
+
) -> Iterator[tuple[str | None, nodes.Call]]:
|
|
61
|
+
"""Calls to ``random`` functions with the name of the target they are assigned to."""
|
|
62
|
+
|
|
63
|
+
def walk(node: Node, target: str | None) -> Iterator[tuple[str | None, nodes.Call]]:
|
|
64
|
+
if isinstance(node, nodes.Function | nodes.Class | nodes.Lambda):
|
|
65
|
+
return
|
|
66
|
+
if isinstance(node, nodes.Assign | nodes.AugAssign):
|
|
67
|
+
target = _target_name(node.target)
|
|
68
|
+
if isinstance(node, nodes.Call):
|
|
69
|
+
symbol = symbols.resolve_expression(scope, node.callee) # type: ignore[attr-defined]
|
|
70
|
+
if symbol in RANDOM:
|
|
71
|
+
yield target, node
|
|
72
|
+
for child in children(node):
|
|
73
|
+
yield from walk(child, target)
|
|
74
|
+
|
|
75
|
+
for statement in function.body:
|
|
76
|
+
yield from walk(statement, None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _target_name(target: nodes.Target) -> str | None:
|
|
80
|
+
if isinstance(target, nodes.Name):
|
|
81
|
+
return target.identifier
|
|
82
|
+
if isinstance(target, nodes.Attribute):
|
|
83
|
+
return target.name
|
|
84
|
+
if isinstance(target, nodes.Subscript) and isinstance(target.key, nodes.Constant):
|
|
85
|
+
return str(target.key.value)
|
|
86
|
+
return None
|