mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 20:19:29 +00:00
python.toml's sources were Flask-only, so no flow could start anywhere on a Django
project. OpenCVE's SSRF reads its URL from `form.cleaned_data`, and the scan answered
the sink file and found nothing. Added: request.GET/POST/body/COOKIES/FILES/META,
DRF's query_params, and cleaned_data.
With those sources known, OpenCVE reported three redirects that cannot leave the
site: `reverse("cves") + "?" + request.GET.urlencode()` (twice) and
`reverse(route, kwargs=...)`. Destination sink facts can now set
`prefix_fixes_origin` and `origin_anchors`. A path is cut when it enters a
concatenation, f-string or `+=` through anything other than its leftmost operand,
and that operand fixes the origin. Origin-fixing operands:
- a literal `/x`, `?` or `#`;
- a literal of the form scheme://host/;
- an anchor call;
- a variable whose every assignment fixes the origin.
A path is also cut when it passes through an anchor. `"/" + x` (//evil) and
`"https://" + host` stay reported. A Joern-gated test covers all seven shapes.
Exploratory (retune after protocol-v1): OpenCVE vulnerable and fixed pins are both
0 findings, down from 2 and 3. The SSRF itself is still missed, because the flow
crosses a registry dispatch (`REGISTRY.get(t)(extras).run()`) that the engine does
not follow.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
165 lines
7.3 KiB
TOML
165 lines
7.3 KiB
TOML
version = "1"
|
|
language = "javascript"
|
|
|
|
[[source]]
|
|
id = "req"
|
|
patterns = ["req.query", "req.body", "req.params", "req.cookies", "req.headers", "request.query"]
|
|
|
|
[[sink]]
|
|
id = "eval"
|
|
cwe = "CWE-95"
|
|
calls = ["eval"]
|
|
rule_ids = ["js-eval"]
|
|
|
|
[[sink]]
|
|
# CWE-95. Node's `vm` module compiles and runs a string exactly as `eval` does -- its own documentation says
|
|
# it is not a security mechanism. mongo-express CVE-2019-10758 is `vm.runInNewContext('doc = eval((' +
|
|
# string + '));')` over a request body, and with only `eval` listed the flow ended at a sink nobody named.
|
|
id = "vm.eval"
|
|
cwe = "CWE-95"
|
|
calls = ["runInNewContext", "runInContext", "runInThisContext", "compileFunction"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
id = "exec"
|
|
cwe = "CWE-78"
|
|
calls = ["exec", "execSync"]
|
|
rule_ids = ["js-command-exec"]
|
|
|
|
[[sink]]
|
|
id = "query"
|
|
cwe = "CWE-89"
|
|
calls = ["query", "execute"]
|
|
rule_ids = ["js-sql-injection"]
|
|
|
|
[[sanitizer]]
|
|
id = "parameterized_query"
|
|
calls = ["query", "execute"]
|
|
parameterized = true
|
|
|
|
# --- model-grounded-detection: raw-Node request sources -------------------------------------------------
|
|
# The shipped `req` source fact carries the Express idioms (req.query, req.body, req.params). A raw Node
|
|
# http server -- which is what a static file server is -- reads the path straight off `req.url`
|
|
# (http.IncomingMessage.url), and `url.parse(req.url)` is the documented way to split it. Measured need:
|
|
# with these absent, the angular-http-server path-traversal pair resolved its flow to the captured `res`
|
|
# rather than to attacker input, because no modelled source matched.
|
|
|
|
[[source]]
|
|
id = "node-request"
|
|
patterns = ["req.url", "request.url", "req.rawHeaders", "url.parse", "parsedUrl"]
|
|
|
|
# --- model-grounded-detection task 4.2 -------------------------------------------------------------------
|
|
# See the note in python.toml: documented dangerous APIs, provenance per entry, reviewed as data.
|
|
|
|
[[sink]]
|
|
# CWE-1321. Prototype pollution reaches the object graph through a recursive merge or an unchecked
|
|
# dynamic key. These are the documented merge helpers plus the assignment forms.
|
|
id = "prototype.merge"
|
|
cwe = "CWE-1321"
|
|
calls = ["merge", "mergeDeep", "extend", "defaultsDeep", "Object.assign", "set", "setWith"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-79. The documented "trusted HTML" sinks in the DOM and the major frameworks.
|
|
id = "html.unescaped"
|
|
cwe = "CWE-79"
|
|
calls = ["innerHTML", "outerHTML", "insertAdjacentHTML", "document.write", "dangerouslySetInnerHTML", "v-html"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-79, the SERVER half. Every sink above is a DOM sink, so this family could only ever ask about code
|
|
# running in a browser -- and the 2026-09-19 family census recorded it completing 44 questions on a Node
|
|
# application and finding nothing, which is exactly what a client-only vocabulary must do there. These are the
|
|
# documented ways an Express or raw-Node handler puts bytes in a response body without an encoder in between.
|
|
#
|
|
# `res.json` is deliberately absent: it sets an application/json content type, so a value reaching it is not
|
|
# a script in a document. `res.render` is deliberately absent too, and for a different reason -- whether a
|
|
# template escapes is a property of the engine's configuration rather than of the call, so it is claimed as a
|
|
# configuration defect (`swig.setDefaults`, `nunjucks.configure` in the obligation facts) and stated as a
|
|
# limit on this family rather than guessed at here.
|
|
id = "html.response"
|
|
cwe = "CWE-79"
|
|
calls = ["res.write", "res.end", "res.send", "response.write", "response.end", "response.send"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-22. Node filesystem entry points that accept a caller-supplied path.
|
|
id = "path.open"
|
|
cwe = "CWE-22"
|
|
calls = ["readFile", "readFileSync", "createReadStream", "sendFile", "unlink", "unlinkSync"]
|
|
rule_ids = []
|
|
|
|
[[sink]]
|
|
# CWE-918 / CWE-601: outbound requests and redirects to a caller-supplied destination.
|
|
#
|
|
# The set shipped with five clients and missed the one the measured repository calls. NodeGoat's documented
|
|
# SSRF is `needle.get(req.query.url + req.query.symbol)` at `app/routes/research.js:16`, and the 2026-09-19
|
|
# family census recorded this family completing 44 questions there, finding the open redirect two files away,
|
|
# and having no way to ask about the request. A thin vocabulary is a silent false negative, so what follows is
|
|
# the client surface a Node server actually calls.
|
|
#
|
|
# Bare where the module ITSELF is the request function, which is the documented call form for all of these
|
|
# (`axios(config)`, `got(url)`, `needle('get', url)`, `request(opts)`), and which also covers every verb
|
|
# method through it: a token is matched word-bounded against the callee, and `axios` is bounded inside
|
|
# `axios.get`. Dotted for `http` and `https`, whose namespace also holds `createServer` -- a server is not an
|
|
# outbound request, and the bare form would have claimed every one of them.
|
|
#
|
|
# Measured on NodeGoat: `needle` matches exactly the research handler's request and nothing else; none of the
|
|
# other clients appears in that repository, in first-party code or vendored.
|
|
id = "outbound.request"
|
|
cwe = "CWE-918"
|
|
calls = [
|
|
"axios", # axios, and axios.get/.post/.put/.patch/.delete/.head/.request through it
|
|
"got", # got
|
|
"needle", # needle
|
|
"request", # request / request-promise
|
|
"superagent", # superagent
|
|
"unirest", # unirest
|
|
"undici", # undici, and undici.fetch/.request through it
|
|
"fetch", # global fetch, node-fetch, cross-fetch
|
|
"http.get",
|
|
"http.request",
|
|
"https.get",
|
|
"https.request",
|
|
]
|
|
rule_ids = []
|
|
prefix_fixes_origin = true
|
|
|
|
[[sink]]
|
|
id = "redirect"
|
|
cwe = "CWE-601"
|
|
calls = ["redirect", "location.assign", "location.replace"]
|
|
rule_ids = []
|
|
prefix_fixes_origin = true
|
|
|
|
# --- model-grounded-detection: sanitizers -----------------------------------------------------------------
|
|
# This file shipped with no [[sanitizer]] entries at all, so no JavaScript fix of any family could be
|
|
# recognised and the model entailed a fixed twin exactly as readily as its vulnerable one. Measured on the
|
|
# angular-http-server path-traversal pair, whose fix is precisely `path.normalize(...).replace(/^(\.\.[\/\\])+/, '')`.
|
|
# Documented normalising / escaping APIs, provenance per entry, reviewed as data.
|
|
|
|
[[sanitizer]]
|
|
# CWE-22. Node's documented path normalisation; `resolve` and `join` against a fixed base collapse `..`.
|
|
id = "path.normalize"
|
|
calls = ["path.normalize", "path.resolve"]
|
|
|
|
[[sanitizer]]
|
|
# CWE-79. The standard encoders: the DOM/global URI encoders and the common escaping helpers.
|
|
id = "html.escape"
|
|
calls = ["encodeURIComponent", "encodeURI", "escapeHtml", "escape", "sanitizeHtml", "DOMPurify.sanitize"]
|
|
|
|
|
|
[[layout]]
|
|
# First-party test conventions are retained as graph context, ordered as unshipped.
|
|
# These declarations cover JS module/JSX and TS/TSX filenames; they do not assert
|
|
# TypeScript property support or a browser/server runtime classification.
|
|
id = "javascript_first_party_tests"
|
|
vendored = ["node_modules/"]
|
|
tests = [
|
|
"tests/", "test/", "spec/", "__tests__/", "__mocks__/", "e2e/",
|
|
"*.test.js", "*.test.mjs", "*.test.cjs", "*.test.jsx", "*.test.ts", "*.test.mts", "*.test.cts", "*.test.tsx",
|
|
"*.spec.js", "*.spec.mjs", "*.spec.cjs", "*.spec.jsx", "*.spec.ts", "*.spec.mts", "*.spec.cts", "*.spec.tsx",
|
|
"*.mock.js", "*.mock.mjs", "*.mock.cjs", "*.mock.jsx", "*.mock.ts", "*.mock.mts", "*.mock.cts", "*.mock.tsx",
|
|
"*.e2e.js", "*.e2e.mjs", "*.e2e.cjs", "*.e2e.jsx", "*.e2e.ts", "*.e2e.mts", "*.e2e.cts", "*.e2e.tsx",
|
|
]
|