fix(scan): a catch-all function's parameters are not attacker input; field reads need the same field

Two classes the traced PMPro survivors named after 1f91c0b:

- The mapper records every named PHP function as an entry of kind
  `function` (3,507 of PMPro's 4,412), and every entry's parameters were
  sources, so internal helpers became injections: sendCancelAdminEmail's
  $user, a gateway hook callback's $user_id/$morder fired by do_action.
  Regions now carry the entry kind; parameters are input only for request
  entries (route, parser, privileged surface). Call depth is unchanged, so
  such a region is still followed into its callees. JavaScript and Python
  emit no `function` entries.
- The object rule is general: a field read continues a flow through the
  bare object only if the taint came through the SAME field, or was
  assigned to the variable or passed in as a parameter. It now also cuts
  $this->Gateway -> this -> $this->user_id (memberorder.php:705).

plausible() memoises the sink test per node and skips the object walk for
paths with no field access (the filter had cost one outlier 101 s -> 160 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-24 02:26:07 +02:00
co-authored by Claude Opus 5.5
parent 1f91c0b089
commit da0ee576f8
4 changed files with 51 additions and 11 deletions
+13
View File
@@ -375,3 +375,16 @@ def test_a_killed_culprit_makes_its_family_dearer_and_nothing_else() -> None:
assert sizer.rate("output_encoding") >= 54.0
assert abs(sizer.rate("injection") - 1.0) < 1e-9
assert sizer.fixed == 25.0, "a killed portion must not be read as a fixed-start sample"
def test_only_a_request_entry_treats_its_parameters_as_attacker_input() -> None:
"""The mapper's `function` kind is its catch-all; its parameters carry whatever the caller had."""
from openultrasast.model import scan
route = ScanRegion("api.php", "handle", "php", ("injection",), 1.0, "entry_point", entry_kind="route")
helper = ScanRegion("mail.php", "sendCancelAdminEmail", "php", ("injection",), 0.3, "entry_point", entry_kind="function")
params = scan._grouped([("0", route, scan._spec_for("injection", "php")), ("1", helper, scan._spec_for("injection", "php"))])["taint"]
assert params["0"]["parameterSources"] == "true"
assert params["1"]["parameterSources"] == "false"
# Still followed into its callees: only its own parameters stop being sources.
assert params["1"]["callDepth"] == params["0"]["callDepth"] != "0"