mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 12:09:27 +00:00
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
57f5a3f0dc
commit
f31ef862a7
@@ -152,11 +152,21 @@ the word `set` in a code comment, matched because the sink text clause searched
|
||||
assignment node. Nothing about this changes the release gates. No capability is qualified by
|
||||
these subjects.
|
||||
|
||||
The first of the three gaps is closed. The untrusted-destination vocabulary now holds the
|
||||
client surface a Node server actually calls, and NodeGoat's SSRF is reported at
|
||||
`app/routes/research.js:16` with a witness naming the call. One data change, no query change,
|
||||
every other family unchanged. Output encoding and access control remain open as tasks 14.3
|
||||
and 14.4.
|
||||
Two of the three gaps are closed. The untrusted-destination vocabulary now holds the client
|
||||
surface a Node server actually calls, and NodeGoat's SSRF is reported at
|
||||
`app/routes/research.js:16` with a witness naming the call. Output encoding now asks about the
|
||||
server half of its own family and establishes a finding at `app/routes/research.js:25`, and it
|
||||
states in its declaration that template-mediated escaping is not something it can decide. Five
|
||||
of seven families now establish findings; access control remains open as task 14.4.
|
||||
|
||||
Closing the second gap surfaced four defects, three of which were false positives it would
|
||||
otherwise have shipped: a permissive-value set shared across settings whose booleans have
|
||||
opposite polarity, a dotted setting matched on its trailing segment alone, a flow query asked of
|
||||
a call's receiver as well as its arguments, and a file's settings ordered by the spelling of
|
||||
their line numbers. Template auto-escaping disabled at `server.js:135` is detected and still not
|
||||
reported, because the arbiter returns one verdict per question and that file has an earlier
|
||||
permissive setting; task 14.6 carries it.
|
||||
|
||||
Evidence: [family detection census](../../../benchmarks/measurements/2026-09-19-family-detection-census.json),
|
||||
[untrusted destination vocabulary](../../../benchmarks/measurements/2026-09-19-untrusted-destination-vocabulary.json).
|
||||
[untrusted destination vocabulary](../../../benchmarks/measurements/2026-09-19-untrusted-destination-vocabulary.json),
|
||||
[output encoding family](../../../benchmarks/measurements/2026-09-19-output-encoding-family.json).
|
||||
|
||||
@@ -481,13 +481,23 @@ Preserve all completed task evidence below.
|
||||
- _Boundary: shipped semantic facts_
|
||||
- _Depends: 14.1_
|
||||
- _Requirements: 6.2, 8.1_
|
||||
- [ ] 14.3 Decide what output encoding claims for server-rendered JavaScript
|
||||
- [x] 14.3 Decide what output encoding claims for server-rendered JavaScript
|
||||
- **Named by the 14.1 census.** All six declared JavaScript sinks are DOM sinks. NodeGoat's XSS is server-side, rendered into Swig templates and written with `res.write`, so the 44 questions this family completed could not have found it whatever the code did.
|
||||
- Either the family gains server-side sinks and template-rendering sinks, or it declares itself client-side only. Both are honest; silence while declaring XSS coverage is not.
|
||||
- Done when one record states the decision, and the family either asks about `res.write` and template rendering or says in its declaration what it does not cover.
|
||||
- _Verified 2026-09-19:_ 1,415 tests passed, thirteen skipped; nine Joern-gated query tests pass in the image; Ruff/format/mypy passed. **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` now says so. The setting is claimed where its evidence is instead, as a configuration defect.
|
||||
- It now 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. Evidence: `benchmarks/measurements/2026-09-19-output-encoding-family.json`.
|
||||
- _Four defects 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, so `swig.setDefaults({autoescape: true})` would have been reported as the defect on the line that repairs it; `ConfigSpec` now carries per-setting values and Jinja2's `Environment` was 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`. `taint.sc` asked its flow query of every argument including the receiver, so `res.end()` — which takes no data — was reported exactly like the `res.write(body)` two lines above it. And the arbiter sorted a file's settings by line number as text, so which setting a file reported depended on the spelling of its line numbers.
|
||||
- _Boundary: shipped semantic facts, family taxonomy_
|
||||
- _Depends: 14.1_
|
||||
- _Requirements: 6.2, 8.1_
|
||||
- [ ] 14.6 Report every permissive setting in a file, not the first one
|
||||
- **Named by the 14.3 measurement.** The configuration arbiter returns one verdict per question. NodeGoat's `server.js` holds a permissive session at line 78 and template auto-escaping disabled at line 135 in the same function, so the escaping defect is detected — measured model_entailed against the real query and the real arbiter — and never reported. A contributor is shown one of two defects and told nothing about the other.
|
||||
- This is the arbiter's contract, not a data gap: `verdict()` returns `Verdict | None` and the scan collects one per question. Changing it touches every family that returns a verdict, so it is scheduled rather than folded into a vocabulary change.
|
||||
- Done when a file with two permissive settings reports both, with their own sites and witnesses, and no family reports a defect twice.
|
||||
- _Boundary: contributor-scan arbitration, Rung ladder_
|
||||
- _Depends: 14.3_
|
||||
- _Requirements: 6.3, 8.1_
|
||||
- [ ] 14.4 Raise the access-control obligation where the application splits route from query
|
||||
- **Named by the 14.1 census and measured with the real dominance query.** Every modelled data operation in NodeGoat lives in `app/data/*-dao.js`; none lives in a route handler file. Operations are scoped to the region's own file and access_control regions are the handler files, so no obligation is raised where the question is asked, and inside the DAO files every operation is equally unguarded so the sibling rung has nothing to contradict. The family answers correctly on VAmPI, which keeps handler and query in one file.
|
||||
- The file scope was chosen deliberately, because unscoped rows attributed a model's unguarded query to a handler of the same name. Widening it is not the fix; carrying the obligation along the call the handler actually makes might be.
|
||||
|
||||
Reference in New Issue
Block a user