mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(semantic-facts): reach the deserialization form WordPress actually writes
Across four WordPress plugins and 1,164 PHP files there is exactly one deserialization site, and
it is spelled maybe_unserialize. The bare token never reached it, and correctly so: the sink
matcher is word-bounded and the character before unserialize inside maybe_unserialize is a word
character. A token a repository never spells is a family that can never answer, which is what the
2026-09-19 census recorded for this one. maybe_unserialize is a real entry point rather than a
safe wrapper: it calls unserialize whenever the string looks serialized.
Also adds the prototype control. That family had never established anything on any subject, and
its only census report was the word `set` inside a comment. No repository available to this
project contains the mechanism: the only first-party matches on the Node subject are
app.set("view engine", "html") and its sibling, Express settings whose arguments are literals. So
the control demonstrates what the family can do, on a merge of the request body into an object,
and asserts the Express setting is not reported alongside it.
Corrects the PHP record under task 13.2. joernio/joern#6281, this project's own report, is a PHP
`global` inside a closure producing a node with two AST parents, which made the dataflow overlay
throw and cost the whole graph. It was filed against 4.0.623 and fixed by #6270 in v4.0.625,
which the image has pinned since before any measurement recorded here. Verified today on the real
trigger rather than on a transcription of the repro: PMPro's includes/fields.php, the file the
original bisection found, builds and applies the default overlays cleanly on 4.0.625 and on
4.0.631 alike. What remains under that task is a runtime failure and must not be re-diagnosed as
the graph bug.
1,417 tests pass, eighteen skipped.
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
3bd844055c
commit
2c5fba55ef
@@ -516,3 +516,62 @@ def test_a_destructured_identity_discharges_what_it_reaches(tmp_path: Path) -> N
|
||||
leaked = answer("displayEverything")
|
||||
assert leaked is not None, "a query that reads the whole collection was not reported"
|
||||
assert leaked.rung is Rung.CORROBORATED, f"{leaked.rung} from {leaked.witness}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(_joern() is None, reason="joern is not installed on this machine")
|
||||
def test_prototype_pollution_is_established_on_the_shape_it_declares(tmp_path: Path) -> None:
|
||||
"""The family had never established anything, on any subject, so this is what it can do.
|
||||
|
||||
The 2026-09-19 census recorded prototype pollution asked on one repository, where its only report was the
|
||||
word `set` inside a comment. No repository available to this project contains the mechanism: the only
|
||||
first-party matches on the Node subject are `app.set("view engine", "html")` and its sibling, which are
|
||||
Express settings whose arguments are literals. So the family's ability to answer is demonstrated here, on
|
||||
a merge of the request body into an object, and the Express setting is asserted alongside it.
|
||||
"""
|
||||
from openultrasast.cpg.backend import extract_payload
|
||||
from openultrasast.model.specs import taint_specs
|
||||
|
||||
frontend = __import__("shutil").which("jssrc2cpg")
|
||||
if frontend is None:
|
||||
pytest.skip("jssrc2cpg is not installed")
|
||||
sinks = taint_specs(language="javascript")["prototype"].sinks
|
||||
|
||||
source = tmp_path / "merge.js"
|
||||
source.write_text(
|
||||
'const _ = require("lodash");\n'
|
||||
"\n"
|
||||
"function handler(req, res, app) {\n"
|
||||
' app.set("view engine", "html");\n'
|
||||
" const options = {};\n"
|
||||
" _.merge(options, req.body);\n"
|
||||
" return res.end();\n"
|
||||
"}\n"
|
||||
)
|
||||
cpg = tmp_path / "cpg.bin"
|
||||
built = subprocess.run([frontend, str(tmp_path), "-o", str(cpg)], capture_output=True, text=True, timeout=600, check=False)
|
||||
if not cpg.is_file():
|
||||
pytest.skip(f"could not build a sample cpg: {(built.stderr or '')[-200:]}")
|
||||
|
||||
requests = tmp_path / "requests.json"
|
||||
requests.write_text(json.dumps({"0": {"sources": "req.body", "sinks": ",".join(sinks), "function": "handler"}}))
|
||||
done = subprocess.run(
|
||||
[
|
||||
_joern() or "joern",
|
||||
"--script",
|
||||
str((QUERIES / "taint.sc").resolve()),
|
||||
"--param",
|
||||
f"cpgFile={cpg}",
|
||||
"--param",
|
||||
f"requestsFile={requests}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=900,
|
||||
check=False,
|
||||
)
|
||||
assert done.returncode == 0, f"taint.sc did not run: {(done.stderr or done.stdout or '')[-500:]}"
|
||||
payload = extract_payload(done.stdout or "")
|
||||
assert payload is not None, "taint.sc produced no parseable payload"
|
||||
flows = [row for row in (payload.get("0") or []) if row.get("sink")]
|
||||
assert flows, "a request body merged into an object was not reported"
|
||||
assert all("merge" in str(row["sink"]) for row in flows), f"the Express setting was reported: {flows}"
|
||||
|
||||
Reference in New Issue
Block a user