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:
Marius Ciepluch
2026-09-20 08:57:32 +02:00
co-authored by Claude Opus 5
parent f31ef862a7
commit 102a21fc4e
6 changed files with 502 additions and 16 deletions
+11 -2
View File
@@ -157,7 +157,15 @@ 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.
of seven families now establish findings.
Access control was the third gap and its structural half is closed. The obligation now follows
the one named call a handler writes into its data-access module, attributed by the graph's own
provenance rather than by name, and located at the call a contributor changed. On NodeGoat it is
raised at six handlers where it was raised nowhere, and three reach a verdict. All three are
corroborated and none is reported without a judge, which is the honest rung: entailing a carried
obligation reported a signup handler that is public by design, because its authenticated
siblings share the same data module. VAmPI is unchanged.
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
@@ -169,4 +177,5 @@ 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),
[output encoding family](../../../benchmarks/measurements/2026-09-19-output-encoding-family.json).
[output encoding family](../../../benchmarks/measurements/2026-09-19-output-encoding-family.json),
[access control across the module split](../../../benchmarks/measurements/2026-09-20-access-control-module-split.json).