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.
Files changed (52) hide show
  1. coretrace_python/__init__.py +1 -1
  2. coretrace_python/bundled/models/aiohttp/aiohttp_models.py +61 -0
  3. coretrace_python/bundled/models/aiohttp/plugin.toml +9 -0
  4. coretrace_python/bundled/models/bottle/bottle_models.py +39 -0
  5. coretrace_python/bundled/models/bottle/plugin.toml +9 -0
  6. coretrace_python/bundled/models/db_drivers/db_driver_models.py +71 -0
  7. coretrace_python/bundled/models/db_drivers/plugin.toml +9 -0
  8. coretrace_python/bundled/models/django/django_models.py +11 -3
  9. coretrace_python/bundled/models/flask/flask_models.py +6 -2
  10. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +12 -6
  11. coretrace_python/bundled/models/tornado/plugin.toml +9 -0
  12. coretrace_python/bundled/models/tornado/tornado_models.py +37 -0
  13. coretrace_python/bundled/syntax/insecure_tempfile/insecure_tempfile.py +25 -0
  14. coretrace_python/bundled/syntax/insecure_tempfile/plugin.toml +9 -0
  15. coretrace_python/bundled/syntax/insecure_tls/insecure_tls.py +40 -0
  16. coretrace_python/bundled/syntax/insecure_tls/plugin.toml +9 -0
  17. coretrace_python/bundled/syntax/unsafe_archive/plugin.toml +9 -0
  18. coretrace_python/bundled/syntax/unsafe_archive/unsafe_archive.py +36 -0
  19. coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +9 -0
  20. coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +35 -0
  21. coretrace_python/bundled/syntax/weak_random/plugin.toml +9 -0
  22. coretrace_python/bundled/syntax/weak_random/weak_random.py +86 -0
  23. coretrace_python/cache.py +14 -1
  24. coretrace_python/cfg/builder.py +7 -2
  25. coretrace_python/cli.py +40 -2
  26. coretrace_python/engine.py +48 -6
  27. coretrace_python/findings/baseline.py +99 -0
  28. coretrace_python/findings/model.py +9 -0
  29. coretrace_python/findings/suppressions.py +71 -0
  30. coretrace_python/frontend/ast_adapter.py +17 -12
  31. coretrace_python/hir/nodes.py +17 -1
  32. coretrace_python/interprocedural/__init__.py +2 -0
  33. coretrace_python/interprocedural/callgraph.py +56 -9
  34. coretrace_python/interprocedural/summaries.py +65 -4
  35. coretrace_python/ir/lowering.py +78 -9
  36. coretrace_python/ir/model.py +24 -0
  37. coretrace_python/ir/printer.py +6 -0
  38. coretrace_python/plugins/detectors.py +23 -10
  39. coretrace_python/plugins/secrets.py +60 -4
  40. coretrace_python/reporters/json_format.py +17 -5
  41. coretrace_python/reporters/report.py +24 -1
  42. coretrace_python/reporters/sarif.py +40 -16
  43. coretrace_python/reporters/text.py +5 -1
  44. coretrace_python/semantic/scopes.py +12 -1
  45. coretrace_python/taint/engine.py +57 -12
  46. coretrace_python/taint/routes.py +54 -3
  47. {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/METADATA +7 -3
  48. {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/RECORD +52 -30
  49. coretrace_python_analyzer-0.2.0.dist-info/licenses/LICENSE +202 -0
  50. coretrace_python_analyzer-0.2.0.dist-info/licenses/NOTICE +4 -0
  51. {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/WHEEL +0 -0
  52. {coretrace_python_analyzer-0.1.0.dist-info → coretrace_python_analyzer-0.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
4
4
 
@@ -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,9 @@
1
+ name = "aiohttp-models"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = []
5
+ provides = ["model.http-sources", "model.aiohttp-routes", "model.ssrf-sinks"]
6
+
7
+ [entrypoint]
8
+ module = "aiohttp_models"
9
+ class = "AiohttpModels"
@@ -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,9 @@
1
+ name = "bottle-models"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = []
5
+ provides = ["model.http-sources", "model.bottle-routes"]
6
+
7
+ [entrypoint]
8
+ module = "bottle_models"
9
+ class = "BottleModels"
@@ -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
+ )
@@ -0,0 +1,9 @@
1
+ name = "db-driver-models"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = []
5
+ provides = ["model.sql-sinks"]
6
+
7
+ [entrypoint]
8
+ module = "db_driver_models"
9
+ class = "DbDriverModels"
@@ -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
- Sink(_sym("django.shortcuts.redirect"), TaintKind.REDIRECT),
111
- Sink(_sym("django.http.HttpResponseRedirect"), TaintKind.REDIRECT),
112
- Sink(_sym("django.http.HttpResponsePermanentRedirect"), TaintKind.REDIRECT),
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
- Sink(_sym("flask.redirect"), TaintKind.REDIRECT),
44
- Sink(_sym("werkzeug.utils.redirect"), TaintKind.REDIRECT),
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 it is given.
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,9 @@
1
+ name = "tornado-models"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = []
5
+ provides = ["model.http-sources", "model.tornado-handlers", "model.ssrf-sinks"]
6
+
7
+ [entrypoint]
8
+ module = "tornado_models"
9
+ class = "TornadoModels"
@@ -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,9 @@
1
+ name = "insecure-temp-file"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = ["interprocedural.callgraph", "ir.ssa"]
5
+ provides = ["vulnerability.insecure-temp-file"]
6
+
7
+ [entrypoint]
8
+ module = "insecure_tempfile"
9
+ class = "InsecureTempFilePlugin"
@@ -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 = "insecure-tls"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = ["interprocedural.callgraph", "ir.ssa"]
5
+ provides = ["vulnerability.insecure-tls"]
6
+
7
+ [entrypoint]
8
+ module = "insecure_tls"
9
+ class = "InsecureTlsPlugin"
@@ -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,9 @@
1
+ name = "unsafe-xml"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = ["interprocedural.callgraph", "ir.ssa"]
5
+ provides = ["vulnerability.unsafe-xml"]
6
+
7
+ [entrypoint]
8
+ module = "unsafe_xml"
9
+ class = "UnsafeXmlPlugin"
@@ -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,9 @@
1
+ name = "weak-random"
2
+ version = "1.0.0"
3
+ plugin_api = ">=1,<2"
4
+ requires = ["semantic.scopes", "semantic.symbols"]
5
+ provides = ["vulnerability.weak-random"]
6
+
7
+ [entrypoint]
8
+ module = "weak_random"
9
+ class = "WeakRandomPlugin"
@@ -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