mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 12:09:27 +00:00
fix(taint): the field join links methods only through the object itself
profile.php:424 survived the hook-argument fix through the field join: in another function of the same file, `$user = new stdClass; $user->ID = $_POST['user_id']`, and the join keys a field by its TEXT across the whole file, so the WP_User parameter's `$user->ID` became a field source. The join exists to carry object state between methods ($this->attachments written in the constructor, read in _delete_files). That reading of a field's text holds only when the base IS the object ($this, this, self). Any other base is a variable, matched within its own method only, where the engine's dataflow already sees it. A real-Joern control covers both (a local variable's field in another function stays quiet; $this state across methods is traced) and fails on the old query. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
53d20691c3
commit
d57966a6b6
@@ -802,3 +802,59 @@ def test_a_filter_returns_its_value_argument_not_its_context(tmp_path: Path) ->
|
||||
flows = {rid: [r for r in payload[rid] if isinstance(r, dict) and r.get("sourceKind")] for rid in ("context", "value")}
|
||||
assert not flows["context"], f"filter context became its result: {flows['context']}"
|
||||
assert flows["value"], "the filtered value no longer carries request data"
|
||||
|
||||
|
||||
@pytest.mark.skipif(_joern() is None, reason="joern is not installed on this machine")
|
||||
def test_the_field_join_links_methods_only_through_the_object_itself(tmp_path: Path) -> None:
|
||||
"""`$this->x` written in one method and read in another is object state; `$user->ID` in two functions is
|
||||
two variables. PMPro's `$user = new stdClass; $user->ID = $_POST['user_id']` in one function made the
|
||||
WP_User parameter's `$user->ID` in another an SQL injection."""
|
||||
from openultrasast.cpg.backend import extract_payload
|
||||
|
||||
frontend = __import__("shutil").which("php2cpg")
|
||||
if frontend is None:
|
||||
pytest.skip("php2cpg is not installed")
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "app.php").write_text(
|
||||
"<?php\n"
|
||||
"function fill() { $user = new stdClass; $user->ID = $_POST['user_id']; return $user; }\n"
|
||||
'function history($user) { global $wpdb; return $wpdb->get_var("SELECT 1 WHERE u = \'" . $user->ID . "\'"); }\n'
|
||||
"class Mailer {\n public $attachments;\n function collect() { $this->attachments = $_POST['files']; }\n"
|
||||
' function send() { global $wpdb; return $wpdb->query("DELETE FROM t WHERE f = \'" . $this->attachments . "\'"); }\n}\n'
|
||||
)
|
||||
cpg = tmp_path / "cpg.bin"
|
||||
built = subprocess.run([frontend, str(src), "-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:]}")
|
||||
common = {
|
||||
"sources": "$_POST",
|
||||
"sinks": "$wpdb->get_var,$wpdb->query",
|
||||
"file": "app.php",
|
||||
"parameterSources": "false",
|
||||
"fieldParameterSources": "true",
|
||||
}
|
||||
requests = tmp_path / "requests.json"
|
||||
requests.write_text(json.dumps({"local": {**common, "function": "history"}, "object": {**common, "function": "send"}}))
|
||||
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,
|
||||
cwd=str(tmp_path),
|
||||
)
|
||||
assert done.returncode == 0, (done.stderr or done.stdout or "")[-500:]
|
||||
payload = extract_payload(done.stdout or "")
|
||||
assert isinstance(payload, dict)
|
||||
flows = {rid: [r for r in payload[rid] if isinstance(r, dict) and r.get("sourceKind")] for rid in ("local", "object")}
|
||||
assert not flows["local"], f"another function's local variable tainted this parameter's field: {flows['local']}"
|
||||
assert flows["object"], "object state written in one method and read in another is no longer traced"
|
||||
|
||||
Reference in New Issue
Block a user