mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(access-control): carry the obligation across the route-and-DAO split
The census measured this family completing 35 questions on NodeGoat with no obligation raised anywhere: every modelled data operation lives in app/data/*-dao.js, none in a route handler file, and operations were scoped to the region's own file. The family had nothing to be right or wrong about. The obligation now follows the one named call the handler writes, out of the region's file and no further. How it is followed is the point. jssrc2cpg does not resolve allocationsDAO.getByUserIdAndThreshold(...) to the DAO body: the callee is an external stub with no code. That stub's fullName records where the member was written, so the implementation is reached by the graph's own provenance rather than by a repository-wide name guess, which is the false positive the file scope was closed against. A control asserts it: a third module defining a member of the same name that nothing imports is not attributed. A row is attributed to the region's DECLARED function, which is what lets a question about displayAllocations match a graph that holds a <lambda>. A carried row is also checked for a guard at the CALL, because that is where a handler establishes who is asking. The method holding a carried operation joins the question's context scope, so the module holding the query cannot change under a claim that depends on it without the change being attributed. The witness names both sites and the finding is located at the call, which is the line a contributor changed. A carried obligation corroborates, it does not entail, and that was measured rather than assumed. Entailing it reported handleSignup, which looks a user up by name to check the name is free during registration and is public by design, because two authenticated siblings in the same file pass req.session.userId into the same module. The entailed rung's justification is that one FILE contradicts itself, same file and same trust context; carried, the compared operations live in a data-access module that handlers of different trust contexts share. The corroborated band already asks the right residual: does this operation require a check, or is the route legitimately public? Measured on NodeGoat: 15 handlers asked, obligations raised at zero before and six now, three reaching a verdict, all corroborated, none reported without a judge. VAmPI unchanged at two entailed findings, because it keeps handler and query in one file and carries nothing. What this does not reach is recorded, not implied. NodeGoat's documented insecure direct object reference still raises nothing, because its module reads with allocationsCol.find(...) and find is not a modelled operation: Array.prototype.find and jQuery's .find are everywhere, so it needs an argument-shape discriminator and its own measurement (task 14.8). Two of the three corroborated claims are correct code whose identity read a destructuring spelled away, and the cross-file contradiction that would entail the real defect needs a sibling-scope decision (task 14.7). 1,415 tests pass, fifteen skipped; eleven 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
f31ef862a7
commit
102a21fc4e
@@ -498,14 +498,33 @@ Preserve all completed task evidence below.
|
||||
- _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
|
||||
- [x] 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.
|
||||
- NodeGoat's two documented defects are also outside what the model can claim: an authenticated handler reading `userId` from `req.params` (`app/routes/allocations.js:17`), and a commented-out `isAdmin` in the route table (`app/routes/index.js:58`). Record which of the two this task is for, and do not silently widen the claim to the other.
|
||||
- Done when a route-and-DAO split raises the obligation at the handler with a witness naming both sites, and a same-name model function is still not attributed to a handler.
|
||||
- _Verified 2026-09-20:_ 1,415 tests passed, fifteen skipped; eleven Joern-gated query tests pass in the image; Ruff/format/mypy passed. **This task is for the first defect, the route-and-DAO split itself.** The obligation now follows the one named call the handler writes, out of the region's file and no further. Attribution is by the graph's own provenance, not by name: jssrc2cpg leaves the callee an external stub, and that stub's fullName records the file and member where the implementation was written, so a module the handler never imports is never consulted. A row is attributed to the region's DECLARED function, which is what lets a question about `displayAllocations` match a graph that holds a `<lambda>`. A carried row is also checked for a guard at the CALL, because that is where a handler establishes who is asking, and the method holding the carried operation joins the question's context scope so the query's own module cannot change unattributed.
|
||||
- Measured on NodeGoat: 15 handlers asked, obligations raised at zero before and at six now, three of which reach a verdict. All three are corroborated and none is reported without a judge. VAmPI is unchanged at two entailed findings, because it keeps handler and query in one file and carries nothing. Evidence: `benchmarks/measurements/2026-09-20-access-control-module-split.json`.
|
||||
- _A carried obligation corroborates, it does not entail, and that was measured rather than assumed._ Entailing it reported `handleSignup`, which looks a user up by name to check the name is free during registration and is public by design, because two authenticated siblings in the same file pass `req.session.userId` into the same module. The entailed rung's justification is that one FILE contradicts itself, same file and same trust context; carried, the compared operations live in a data-access module that handlers of different trust contexts share. The corroborated band already asks the right residual: does this operation require a check, or is the route legitimately public?
|
||||
- _What this does not reach, recorded rather than implied:_ NodeGoat's documented IDOR at `app/routes/allocations.js:17` still raises nothing, because its module reads with `allocationsCol.find(...)` and `find` is not a modelled operation (task 14.8). The commented-out `isAdmin` at `app/routes/index.js:58` is not claimable by this abstraction at all, since the evidence that `/benefits` needs a stronger guard than its siblings exists only in a comment.
|
||||
- _Boundary: contributor-scan dominance, Ranker Scope Contract_
|
||||
- _Depends: 14.1_
|
||||
- _Requirements: 6.2, 6.3, 8.1_
|
||||
- [ ] 14.7 Decide the sibling scope, and see identity that a destructuring spelled away
|
||||
- **Named by the 14.4 measurement.** Two of NodeGoat's three corroborated claims are correct code: `const { userId } = req.session` followed by `dao.getByUserId(userId, ...)` is identity-constrained, and the destructuring erases the `session.userId` spelling the token list holds, so the guard is invisible at the call. A judge would be asked about handlers that are not defects.
|
||||
- The contradiction that would make NodeGoat's IDOR entailed is cross-file: `allocations` passes a URL parameter while `contributions` and `profile` pass session identity. The sibling set is one file, and widening it is a decision, not an implementation detail: measured, a route file mixes public with authenticated handlers, so application-wide siblings would contradict every login and signup handler there is.
|
||||
- Record the measured cost of each candidate before choosing, and do not widen the scope to make a finding appear.
|
||||
- Done when one record states the candidates and the choice, a destructured identity read discharges the obligation it actually discharges, and no public route is reported.
|
||||
- _Boundary: contributor-scan dominance, shipped obligation facts_
|
||||
- _Depends: 14.4_
|
||||
- _Requirements: 6.2, 6.3, 8.1_
|
||||
- [ ] 14.8 Model the collection read an application actually writes
|
||||
- **Named by the 14.4 measurement.** NodeGoat's documented insecure direct object reference reads with `allocationsCol.find(searchCriteria())`, and `find` is absent from an operation vocabulary holding `findOne`, `findAll` and `findById`. It is the commonest read there is, so the family's obligation is missing at exactly the shape it exists for.
|
||||
- It is absent for a reason: `Array.prototype.find` is everywhere in JavaScript and jQuery's `.find` is everywhere in a vendored bundle. A collection read takes a query document where those take a closure or a selector, so the discriminator is the argument's shape rather than the name.
|
||||
- Done when a collection read raises the obligation, an array search and a selector do not, and the false-positive count is measured on a repository with vendored bundles rather than argued from the shape alone.
|
||||
- _Boundary: shipped obligation facts, contributor-scan dominance_
|
||||
- _Depends: 14.4_
|
||||
- _Requirements: 6.2, 8.1_
|
||||
- [ ] 14.5 Ask the prototype and deserialization families somewhere they can answer
|
||||
- **Named by the 14.1 census.** Prototype pollution has been asked on one repository and its only report was a false positive; deserialization was never asked at all, because VAmPI holds no such sink and NodeGoat is not a language where the family is declared. Neither has established anything anywhere.
|
||||
- Find or record a subject where the family has a sink, and report what it does. A negative on a subject that contains the mechanism is a result; silence on a subject that does not contain it is not.
|
||||
|
||||
Reference in New Issue
Block a user