Files
Open_Ultra_SAST/src/openultrasast/ruleset/semantic/javascript.toml
T
Marius CiepluchandClaude Opus 5 f31ef862a7 fix(output-encoding): ask about the server half, and say what the family cannot decide
The census recorded this family completing 44 questions on a Node application and finding
nothing, which is what a client-only vocabulary must do there: all six declared sinks were DOM
sinks. NodeGoat's cross-site scripting is server-side.

Decision: the family asks about a value reaching a rendering call, and states in its own
declaration that it cannot decide template-mediated escaping. Whether res.render is safe depends
on the engine's autoescape setting and on the template's markup, neither of which is at the
call, so res.render is not a sink and the family's limits field says so. A new CWE-79 sink
covers res.write, res.end, res.send and the response.* spellings; res.json stays out because an
application/json body is not a document.

Template auto-escaping is claimed where its evidence is, as a configuration defect:
swig.setDefaults, nunjucks.configure and marked.setOptions are security settings now.

It establishes a finding: app/routes/research.js:25, model_entailed,
`req.query.url + req.query.symbol -> res.write(body)`. The handler fetches a URL the caller
chooses and writes the response body into a text/html response, so the document served from
this origin is the attacker's. The record states how coarse the step is: the path from url to
body runs through the client's callback parameter, which the graph treats as reachable from the
call's arguments, so the conclusion rests on the call's semantics rather than on what the graph
established. Five of seven families now establish findings.

Four defects were found on the way, three of them false positives this change would otherwise
have shipped.

- The permissive-value set was shared across every setting, while a boolean's polarity belongs
  to the setting: true leaves the door open in cors({origin: true}) and closes it in
  swig.setDefaults({autoescape: true}). Adding the template engines would have reported the fix
  as the defect on the line that offers it. ConfigSpec now carries per-setting values, and
  Jinja2's Environment moved out of Python's shared list for the same latent reason.
- config.sc matched a dotted setting on its trailing segment alone, so marked.setOptions would
  have claimed an editor's setOptions. dominance.sc already required the qualifier.
- taint.sc asked its flow query of every argument including the receiver at index 0. On a
  response sink the receiver is the response object, so res.end() -- which takes no data -- was
  reported exactly like the res.write(body) two lines above it. Flows now use the real
  arguments, as arity already did.
- The arbiter sorted a file's settings by line number as TEXT, so line 126 preceded line 78 and
  which setting a file reported depended on the spelling of its line numbers.

Template escaping disabled at server.js:135 is detected, measured model_entailed against the
real query and the real arbiter, and still not reported: the arbiter returns one verdict per
question and that file has an earlier permissive setting. Task 14.6 carries it rather than
hiding it.

1,415 tests pass, thirteen skipped; nine Joern-gated query tests pass in the image.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-09-19 20:38:39 +02:00

154 lines
6.8 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]]
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 = []
[[sink]]
id = "redirect"
cwe = "CWE-601"
calls = ["redirect", "location.assign", "location.replace"]
rule_ids = []
# --- 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",
]