mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
feat(access-control): model the collection read, by argument shape rather than by name
NodeGoat's documented insecure direct object reference reads with allocationsCol.find(searchCriteria()), and `find` was absent from an operation table holding findOne, findById, findAll and findByPk. The obligation was missing at the commonest read there is, so that defect raised nothing at any point in the pipeline. It was absent for a good reason, and the reason is the argument, not the name. Measured on wp-statistics, a WordPress plugin that ships jQuery: its assets hold 88 calls named find, of which 70 pass a jQuery selector, 13 pass a predicate to Array.prototype.find and 5 pass a selector built at run time by a template literal or a concatenation. None is a collection read. So the obligation is claimed only where the first argument has the shape of a query document. The operation fact states which argument with query_document_arg, and the arbiter that can see argument shapes decides it: no arguments at all, an object literal, or a builder call are claimed; a method reference, a literal, an operator call and a bare identifier are not. Each exclusion is one of the measured classes above rather than a guess. Zero of those 88 calls became an obligated operation, and the zero is measured rather than empty: the same run's census reports 60 files and 4,552 methods, and the files holding those calls are in it. On NodeGoat all three find calls are collection reads and all three are claimed, which raises the obligation at four more handlers -- the allocations handler that takes userId from req.params on an authenticated route, the benefits pages whose isAdmin guard is commented out in the route table, and the memo listing. Seven of fifteen handlers now reach a verdict, all corroborated, none reported without a judge, and the census is unchanged on both subjects. A bare identifier is deliberately not claimed: col.find(query) and arr.find(pred) are the same shape and neither repository measured holds one, so this is a stated false negative rather than an unmeasured false-positive risk. 1,415 tests pass, sixteen skipped; twelve 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
102a21fc4e
commit
c0427e6ceb
@@ -436,3 +436,38 @@ def test_an_identity_constrained_handler_is_not_reported(tmp_path: Path) -> None
|
||||
cpg = CpgResult(cpg_path=tmp_path / "cpg.bin", run=lambda q, p: rows)
|
||||
answer = verdict(cpg, spec, function="displayMine", file="app/routes/notes.js")
|
||||
assert answer is None, f"a handler that passes the caller's own identity was reported: {answer}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(_joern() is None, reason="joern is not installed on this machine")
|
||||
def test_only_a_collection_read_is_an_obligated_find(tmp_path: Path) -> None:
|
||||
"""`find` is a collection read, an array search and a jQuery selector. Only the first is obligated.
|
||||
|
||||
The name was absent from the operation table for that reason, which left the family's obligation missing at
|
||||
the commonest read there is: NodeGoat's documented insecure direct object reference reads with
|
||||
`allocationsCol.find(searchCriteria())`. Measured on one WordPress plugin's shipped assets, 83 of 88 calls
|
||||
named `find` pass a selector or a predicate, and five pass a selector built at run time. Each exclusion
|
||||
below is one of those measured classes.
|
||||
"""
|
||||
from openultrasast.model.specs import dominance_specs
|
||||
|
||||
spec = dominance_specs(language="javascript")["access_control"]
|
||||
assert "find" in spec.document_shape, "the shape requirement is no longer declared for this call"
|
||||
|
||||
source = tmp_path / "shapes.js"
|
||||
source.write_text(
|
||||
"function shapes(db, arr, $el, pred, query) {\n"
|
||||
' const col = db.collection("notes");\n'
|
||||
" const read = col.find({ owner: 1 });\n"
|
||||
" const wide = col.find();\n"
|
||||
" const built = col.find(criteria());\n"
|
||||
" const search = arr.find(x => x.id === 1);\n"
|
||||
' const selector = $el.find(".cls");\n'
|
||||
' const interpolated = $el.find(`li[data-key="${pred}"]`);\n'
|
||||
" const concatenated = $el.find('li[data-key=\"' + pred + '\"]');\n"
|
||||
" return [read, wide, built, search, selector, interpolated, concatenated];\n"
|
||||
"}\n"
|
||||
)
|
||||
rows = _dominance_rows(tmp_path, function="shapes", file="shapes.js")
|
||||
lines = sorted(int(str(row["opLine"])) for row in rows)
|
||||
# 3 the object literal, 4 the whole collection, 5 the builder call.
|
||||
assert lines == [3, 4, 5], f"the wrong find shapes were claimed: {[(r['opLine'], r['operation']) for r in rows]}"
|
||||
|
||||
Reference in New Issue
Block a user