mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(cpg): taint a field where it is assigned — the field half of 5.11
`_delete_files()` takes no parameters. CVE-2023-6559 arrives as a FIELD,
written by one method and read by another:
public function __construct( ..., array $attachments = array() )
$this->attachments = $attachments; // half one ends here
protected function _delete_files()
foreach ( $this->attachments as $file ) // half two starts here
unlink( $file );
Both halves were already in the graph. Only the join was missing, and the key
it joins on is the literal `$this->attachments`, written identically at both
ends -- so this is a second question asked with the query that already exists,
not an edge a frontend has to invent.
A read of `$this->F` is a source when some assignment to the same literal
`$this->F` in the same file is fed by an unsanitized flow from a seed. Scoped
three ways so a field name common to every class ever written cannot become a
wormhole: only fields the sink scope reads, only assignments in the same file,
and the right-hand side must itself be reachable from a source.
The receiver could then leave `parameterNodes` -- the precision fix refused
two commits ago, now sound, because the path it was standing in for is carried
by the field it actually travels through:
before after
mwwpform vulnerable 1, from `this` 1, from $this->attachments
mwwpform fixed 0 0
pmpro vulnerable 15 -- 4 named, 11 receiver 18 -- all named or field
pmpro fixed 1, receiver-sourced 1
VAmPI 4 entailed 4 entailed, unchanged
CVE-2023-23488 is still at class.memberorder.php:936 and CVE-2023-6559 at
class.mail.php:259. Every finding now names the value it followed instead of
naming the object.
Three things cost a measurement each, recorded in the task:
* A summary must carry the SANITIZATION status of the half it summarises,
not merely its reachability. Without that clause,
`$this->sqlQuery = "..." . esc_sql($x) . "..."` reads as tainted -- and
PMPro builds most of its queries that way, so the fixed side went from 1
finding to 13. That is the pair no longer separating, which is the only
number that matters.
* Two-phase `reachableBy` then `reachableByFlows` is SLOWER, not faster:
183s -> 256s. Running the analysis twice costs more than skipping path
reconstruction saves. Reverted.
* Memoizing per field rather than per file took it to 97s, about 1.6x the
pre-field cost for that class, because only fields some region reads are
worth the flow query.
`inScope` is factored out of `sinkCalls` so the field sources are scoped to
exactly the same place as the sinks, rather than a second copy of those three
branches drifting from the first.
841 tests pass; gates byte-identical; VAmPI unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
e41d6dd7ea
commit
dcdbe322a1
+23
-10
@@ -81,17 +81,19 @@ side because the fix is a call the fact tables already carry: `$wpdb->prepare` f
|
||||
other. That is the distinction worth teaching. The engine is not recognising a bug; it is failing to find a
|
||||
cleansing call on a path it resolved, and then finding one.
|
||||
|
||||
The precision is not clean, and saying so is the point. On PMPro's order class the vulnerable side reports
|
||||
**15 entailed findings**, of which **4** are sourced from a named parameter and **11** from the receiver,
|
||||
`$this`. One receiver-sourced finding survives into 2.9.8 at a query whose every fragment is wrapped in
|
||||
`esc_sql` — a false positive, and a legible one: the flow reaches the sink through `$this->sqlQuery`, so the
|
||||
sanitizer is on the fragments and not on the path.
|
||||
Every finding now names the value it followed. On PMPro's order class the vulnerable side reports **18
|
||||
entailed findings** and the fixed side **1**, and each one is sourced either from a named parameter (`$id`,
|
||||
`$code`, `$token`) or from a named field (`$this->sqlQuery`, `$this->Email`). None is sourced from `$this`.
|
||||
|
||||
Excluding the receiver was tried and reverted. It cuts that file to 6 findings, all named-parameter sourced,
|
||||
and makes the PMPro pair separate cleanly — and it **loses CVE-2023-6559**, because `_delete_files()` takes
|
||||
no parameters at all and its attacker-controlled path arrives as `$this->attachments`, assigned by another
|
||||
method. Trading a verified CVE for findings merely suspected of being false is the wrong trade. The real
|
||||
defect is field-sensitivity: a field should be tainted where it is assigned.
|
||||
That took two changes, and the order between them is the whole point. Excluding the receiver from parameter
|
||||
sources was tried **first** and reverted: it cuts the file to 6 findings and makes the pair separate cleanly,
|
||||
and it **loses CVE-2023-6559**, because `_delete_files()` takes no parameters at all and its
|
||||
attacker-controlled path arrives as `$this->attachments`, assigned by another method entirely. Trading a
|
||||
verified CVE for findings merely suspected of being false is the wrong trade.
|
||||
|
||||
With the field half of the join in place (below), that path is carried by the field it actually travels
|
||||
through, and the receiver no longer has to stand in for it — so it can go, and the eleven receiver-sourced
|
||||
findings go with it. The precision fix became available once the mechanism underneath it was right.
|
||||
|
||||
### The one it misses, and why
|
||||
|
||||
@@ -124,6 +126,17 @@ synthetic endpoint, build the table by reading literals rather than inferring an
|
||||
carries the join's uncertainty — one registered callback and a literal key entails, several callbacks or a
|
||||
computed hook name corroborates. Task 5.11 states it, with both pinned pairs as its observable.
|
||||
|
||||
**The field half is implemented and measured.** `$this->attachments` is tainted at the assignment that fills
|
||||
it, joined to the read by the literal field code, so CVE-2023-6559 is found as
|
||||
`class.mail.php:259:_delete_files ← $this->attachments` and is absent on the fixed side. One thing it needed
|
||||
that is easy to get wrong: the summary has to carry the **sanitization** status of the half it summarises,
|
||||
not merely its reachability. Asking only "does a source reach this assignment" marks
|
||||
`$this->sqlQuery = "..." . esc_sql($x) . "..."` tainted, and PMPro builds most of its queries that way — the
|
||||
fixed side went from 1 finding to 13 before that clause existed, which is the pair no longer separating at
|
||||
all. It costs about 1.6× on the taint query for that class (62s → 97s), memoised per field.
|
||||
|
||||
The hook half is the same machinery against a different key, and is not written yet.
|
||||
|
||||
What the engine reports on that plugin instead is two other sites — `getTop:406` and `TotalCount:443` — the
|
||||
same weakness class at the wrong lines, identical on both sides, so the pair does not separate. They are not
|
||||
noise: upstream now annotates both with `// phpcs:ignore WordPress.DB.PreparedSQL...`, and later rewrote
|
||||
|
||||
Reference in New Issue
Block a user