mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 12:09:27 +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
@@ -590,7 +590,7 @@
|
||||
- _Requirements: 4.2, 4.3, 9.1_
|
||||
- _Depends: 5.7_
|
||||
|
||||
- [ ] 5.11 Two-stage taint with a link table — the detection strategy for both structural misses
|
||||
- [~] 5.11 Two-stage taint with a link table — the detection strategy for both structural misses
|
||||
- "Structurally impossible" is a verdict, not a plan, and both PHP structural misses turn out to be the
|
||||
SAME shape: two halves of a path, joined by a key that is a literal in the source text.
|
||||
|
||||
@@ -634,6 +634,28 @@
|
||||
count down from 11 to 0 with its CVE still found.
|
||||
- Order: the field half first. It is the smaller link table, it has a pinned pair of its own, and it is
|
||||
what unlocks the receiver change; the hook half then reuses the same two-stage machinery.
|
||||
- **Field half done 2026-09-09.** `taint.sc` gains `fieldSourceNodes`: 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. The receiver then left `parameterNodes`, which is only sound because of it.
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| `mwwpform` vulnerable | 1, sourced from `this` | 1, sourced from `$this->attachments` |
|
||||
| `mwwpform` fixed | 0 | 0 |
|
||||
| `pmpro` vulnerable | 15 — 4 named, 11 receiver | 18 — all named or field, CVE still at :936 |
|
||||
| `pmpro` fixed | 1, receiver-sourced | 1 |
|
||||
| VAmPI | 4 entailed | 4 entailed, unchanged |
|
||||
|
||||
Three things learned, each of which cost a measurement:
|
||||
- **A summary must carry the sanitization status of its half, not just reachability.** Without that,
|
||||
`$this->sqlQuery = "..." . esc_sql($x)` reads as tainted and PMPro's fixed side went 1 -> 13. That is
|
||||
the pair not 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.
|
||||
- Memoizing per **field** rather than per file took it to 97s, because only fields some region actually
|
||||
reads are worth the flow query. Roughly 1.6x the pre-field cost for that class.
|
||||
- Still open: the hook half, and the one finding that survives on PMPro's fixed side
|
||||
(`saveOrder:1469 <- $this->timestamp`).
|
||||
- _Requirements: 4.4, 6.1, 8.1, 9.1_
|
||||
- _Depends: 5.8_
|
||||
|
||||
|
||||
Reference in New Issue
Block a user