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
+5
View File
@@ -66,6 +66,10 @@ class ScanRegion:
# Does the project's own build declaration name this file? True when it declares nothing at all --
# silence is not exclusion, and no repository is penalised for a build system we cannot read.
shipped: bool = True
# What the mapper called the entry: `route`, `parser`, `privileged_surface`, ... or `function`, which is
# its catch-all for a named function with no evidence of receiving a request. Only the scan's parameter
# rule reads it, because only it depends on whether a parameter carries request data.
entry_kind: str = ""
def regions_for(entries: Sequence[object], targets: Sequence[object], *, shipped: frozenset[str] | None = None) -> tuple[ScanRegion, ...]:
@@ -126,6 +130,7 @@ def regions_for(entries: Sequence[object], targets: Sequence[object], *, shipped
families=families,
rank=rank,
source="module_scope" if module_body else "entry_point",
entry_kind=str(getattr(entry, "kind", "") or ""),
)
# A nameless entry point IS the file. Where a named region already covers that file it adds nothing but