mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
Three of the four fixed-side alerts left in the independent population had one cause: the fix was a check, and checks were not modelled. - wger's fix is `url_has_allowed_host_and_scheme`: `if not ...(next_url): next_url = reverse(..)` and `if next_url and ...(next_url): return redirect`. - Ultimate Member's fix is an allowlist: `in_array(strtoupper($order), [...], true) ? $order : 'ASC'` and `elseif (in_array($sortby, $allowed, true))`. (The fourth, Signal K, is `assertAllowedHost`, a project-local function. It is not covered.) Sanitizer facts can now say `guard = true`. A guard is a check, never a cleansing call on a path. A flow is discharged only when a guard tests a variable the path carries, and the flow is in one of these places: - inside the branch whose condition requires the check. The guard may be combined with `&&` only; - past a failure test, a single `!` combined with `||` only, whose branch exits (return, exit, die, wp_die, throw) or overwrites the value, and whose condition dominates the flow's end; - through a ternary whose condition requires the check, where the other arm cannot carry the value. The engine can enter through the condition's own copy of the value, which never becomes the result. Facts: `in_array` (PHP) and Django's `url_has_allowed_host_and_scheme` / `is_safe_url` (Python). A row now consults guards even when a request lists no sanitizers. Two Joern-gated tests cover the discharged shapes and, as controls, an unguarded redirect, a guard on a different variable, a failure that is only logged, the unguarded else branch, and the ternary arm the check does not select. Both tests fail without the guard. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
148 lines
4.6 KiB
TOML
148 lines
4.6 KiB
TOML
version = "1"
|
|
language = "python"
|
|
|
|
[[source]]
|
|
id = "request"
|
|
patterns = [
|
|
"request.args",
|
|
"request.form",
|
|
"request.data",
|
|
"request.json",
|
|
"request.cookies",
|
|
"request.values",
|
|
"request.get_json",
|
|
"request.headers",
|
|
"request.files",
|
|
"request.get",
|
|
]
|
|
|
|
# Django and Django REST framework. The source vocabulary above is Flask's alone, so on a Django project no
|
|
# flow could begin anywhere: OpenCVE's notification test POSTs to a URL read from `form.cleaned_data`, and the
|
|
# scan answered the file and found nothing. `cleaned_data` is what a form bound to the request hands back --
|
|
# validated for type, not for trust.
|
|
[[source]]
|
|
id = "django-request"
|
|
patterns = [
|
|
"request.GET",
|
|
"request.POST",
|
|
"request.body",
|
|
"request.COOKIES",
|
|
"request.FILES",
|
|
"request.META",
|
|
"request.query_params",
|
|
"cleaned_data",
|
|
]
|
|
|
|
[[source]]
|
|
id = "argv"
|
|
patterns = ["sys.argv"]
|
|
|
|
[[source]]
|
|
id = "stdin"
|
|
patterns = ["sys.stdin", "stdin.read"]
|
|
|
|
[[source]]
|
|
id = "input"
|
|
patterns = ["input("]
|
|
|
|
[[sink]]
|
|
id = "eval"
|
|
cwe = "CWE-95"
|
|
calls = ["eval", "exec"]
|
|
rule_ids = ["python-unsafe-eval"]
|
|
|
|
[[sink]]
|
|
id = "pickle.loads"
|
|
cwe = "CWE-502"
|
|
calls = ["pickle.loads", "pickle.load", "yaml.load", "marshal.loads", "cPickle.loads"]
|
|
rule_ids = ["python-unsafe-deserialization"]
|
|
|
|
[[sink]]
|
|
id = "execute"
|
|
cwe = "CWE-89"
|
|
calls = ["execute", "executemany"]
|
|
rule_ids = ["python-sql-injection"]
|
|
|
|
[[sink]]
|
|
id = "os.system"
|
|
cwe = "CWE-78"
|
|
calls = ["os.system", "os.popen", "subprocess.run", "subprocess.call", "subprocess.Popen", "subprocess.check_output"]
|
|
rule_ids = ["python-os-command", "python-shell-true"]
|
|
|
|
[[sink]]
|
|
id = "hashlib"
|
|
cwe = "CWE-327"
|
|
calls = ["hashlib.md5", "hashlib.sha1", "hashlib.new"]
|
|
rule_ids = ["python-weak-hash"]
|
|
weak_literals = ["md5", "sha1", "MD5", "SHA1", "sha-1"]
|
|
|
|
[[sanitizer]]
|
|
id = "ast.literal_eval"
|
|
calls = ["ast.literal_eval"]
|
|
|
|
[[sanitizer]]
|
|
id = "yaml.safe_load"
|
|
calls = ["yaml.safe_load", "safe_load"]
|
|
|
|
[[sanitizer]]
|
|
id = "parameterized_execute"
|
|
calls = ["execute", "executemany"]
|
|
parameterized = true
|
|
|
|
# --- model-grounded-detection task 4.2 -------------------------------------------------------------------
|
|
# Sinks for the four families that previously had no fact routing to them by CWE, plus the ORM raw-SQL forms
|
|
# the Joern go/no-go measured as misses (11 of 23). Every entry below is a documented dangerous API of the
|
|
# named library, not an inferred pattern; the provenance is on each entry so a maintainer can review the data
|
|
# as data. `rule_ids` is left empty where no pattern rule exists: these facts feed the model layer, and a
|
|
# sink fact is not a detection rule.
|
|
|
|
[[sink]]
|
|
# CWE-89. Django's documented raw-SQL escape hatches (docs: "Performing raw SQL queries") and the
|
|
# SQLAlchemy/peewee text forms. These were the single largest miss class in the go/no-go.
|
|
id = "orm.raw"
|
|
cwe = "CWE-89"
|
|
calls = ["objects.raw", "raw", "RawSQL", "extra", "text", "from_statement"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-22. Filesystem entry points that accept a caller-supplied path: the stdlib openers plus Flask's
|
|
# send_file/send_from_directory and Django's FileResponse.
|
|
id = "path.open"
|
|
cwe = "CWE-22"
|
|
calls = ["open", "send_file", "send_from_directory", "FileResponse", "os.remove", "os.unlink", "shutil.copy", "shutil.move", "shutil.rmtree"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-79. The documented "this string is trusted HTML" escapes: Jinja/Flask render_template_string and
|
|
# Markup, Django's mark_safe and format_html. Rendering a template *file* is not a sink; rendering a
|
|
# caller-supplied *string* is.
|
|
id = "html.unescaped"
|
|
cwe = "CWE-79"
|
|
calls = ["render_template_string", "Markup", "mark_safe", "format_html", "HttpResponse"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-918 (SSRF) and CWE-601 (open redirect): outbound requests and redirects to a caller-supplied
|
|
# destination. requests/urllib/httpx are the documented HTTP clients; redirect is Flask's and Django's.
|
|
id = "outbound.request"
|
|
cwe = "CWE-918"
|
|
calls = ["requests.get", "requests.post", "requests.request", "urlopen", "urlretrieve", "httpx.get", "httpx.post"]
|
|
rule_ids = []
|
|
prefix_fixes_origin = true
|
|
|
|
[[sink]]
|
|
id = "redirect"
|
|
cwe = "CWE-601"
|
|
calls = ["redirect", "HttpResponseRedirect", "url_for"]
|
|
rule_ids = []
|
|
# Django's `reverse` builds an in-site path from a route NAME; request data can only fill its path segments.
|
|
prefix_fixes_origin = true
|
|
origin_anchors = ["reverse", "reverse_lazy"]
|
|
|
|
# Django's documented redirect-target check (`is_safe_url` is its pre-3.0 name). wger's fix for its open
|
|
# redirect is `if not url_has_allowed_host_and_scheme(next_url, ...): next_url = reverse(...)`.
|
|
[[sanitizer]]
|
|
id = "url_allowlist"
|
|
calls = ["url_has_allowed_host_and_scheme", "is_safe_url"]
|
|
guard = true
|