mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 12:09:27 +00:00
dcdbe322a1e1498639636b478bd38229e120844d
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dcdbe322a1 |
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
|
||
|
|
e41d6dd7ea |
docs(contributor-scan): 5.11 -- a detection strategy, not a verdict of impossible
"Structurally impossible" is a verdict, and a verdict is not a plan. Replacing
both structural-miss tasks with the one mechanism that covers them, because
they are the same shape: two halves of a path joined by a key that is a
literal in the source text.
hook: $_REQUEST -> ... -> return of set_current_page
key: 'wp_statistics_current_page' literal at both ends
return of apply_filters(key) -> ... -> $wpdb->get_row
field: $_POST -> ... -> assignment to $this->attachments
key: attachments a field name at both ends
read of $this->attachments -> ... -> unlink
Neither half is missing from the graph. Only the join is. So this is not a
request for a frontend to draw an edge it cannot draw -- it is a second
question asked with the query that already exists:
stage 1 the existing taint question against a synthetic endpoint: does
untrusted data reach a callback's return, or an assignment to
$this->F?
table built by reading literals, never inferring: add_filter('H', c)
against apply_filters('H', ...); $this->F = against $this->F
stage 2 does the apply_filters return, or the field read, reach a sink?
join on the key, with the RUNG carrying the join's uncertainty -- one
registered callback and a literal key entails; several callbacks or
a computed hook name corroborates, and the witness says which
"Any hook may reach any handler" is refused absolutely: a complete graph over
the plugin would entail everything.
No new dependency, no CPG mutation, no IR -- both stages ride the batch that
already runs, which is the point of expressing it as a query-level join.
It also recovers the precision fix 5.8 had to refuse. With the field half
working, _delete_files() is reached through $this->attachments rather than
through $this being tainted wholesale, so the receiver can then leave
parameter sources and take PMPro's 11 receiver-sourced findings with it.
5.8's numbers are the before-side of that measurement, and both observables
are pairs already pinned.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
|
||
|
|
435c14f3ec |
docs(contributor-scan): 5.8 -- WordPress class by class, including what it cannot ask
Three plugins, each pinned at a version whose advisory was read out of the
code, each measured against its own fixed commit:
unauth injection, sink in the receiving function pmpro FOUND
arbitrary file operation mwwpform FOUND
unauth injection across a WordPress hook wpstatistics MISSED
missing capability check -- cannot be asked
unescaped output -- cannot be trusted
Both hits are entailed by the graph alone -- no judge, no spend -- and both
disappear on the fixed side, because both fixes are calls the fact tables
carry: `$wpdb->prepare` and `realpath()`. The engine is not recognising a bug;
it is failing to find a cleansing call on a path it resolved, then finding one.
Precision is stated rather than flattered. PMPro's vulnerable side reports 15
entailed, 4 from a named parameter and 11 from the receiver, and one of those
survives into 2.9.8 at a query whose every fragment is `esc_sql`-wrapped.
The miss is the useful half, and it is checkable rather than inferred. WP
Statistics' CVE has a modelled source, a modelled sink and `esc_sql` as its
fix, and reports nothing on either side, because the file holding the sink has
zero superglobals, makes no call into the class holding the source, and is
joined to it only by
add_filter('wp_statistics_current_page', array($this, 'set_current_page'));
return apply_filters('wp_statistics_current_page', $current_page);
Both ends name the callback with a string, so no frontend can draw the edge.
Most plugin data travels this way. What the engine reports there instead is two
other sites, the same class at the wrong lines, identical on both sides -- and
not noise: upstream annotates both `// phpcs:ignore WordPress.DB.PreparedSQL`
and later rewrote one with `$wpdb->prepare`.
Two classes could not be asked at all, which is not the same as a miss.
`dominance_specs(language="php")` is empty, so missing-capability is never
asked; and `output_encoding` shares the one flat sanitizer list, which holds
`esc_sql` and `intval` but not `esc_html` -- wrong in both directions, so no
XSS verdict from this engine currently means anything.
5.10's root cause is recorded too: php2cpg's parser bridge, a race rather than
a scale limit, and the mitigation is per-file exclusion. Adds 5.11 (the hook
edge, with CVE-2022-25148 as its observable) and 5.12 (taint a field where it
is assigned, with CVE-2023-6559 as its guard).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
|
||
|
|
8c387306be |
feat(repos): pinned known-vulnerable checkouts, offline by default
The pair corpus scores excerpts: a vulnerable function and its fixed twin, handed to the engine with the answer already narrowed down. Nothing in it exercises a repository -- region ranking, the budget, one CPG over thousands of files, or whether a verdict that holds on fifteen lines still holds when the source, the sink and the guard live in three modules. benchmarks/repos/<name>.toml names a real checkout: url, a full 40-char commit SHA, licence, and every known vulnerability with its file, function and line. `ousast repos` lists them; `--fetch` materialises one; CI does neither, and conftest clears the switch and redirects the cache so a developer with warm checkouts cannot read them by accident. Three rules the loader enforces rather than documents: - A branch or tag is rejected. Either would make the same recipe mean different code next month while the baseline it produced kept the old numbers. - `in_scope` must be stated per vulnerability, never defaulted. It decides whether "not found" is a miss or a non-question, which is the most important thing about the result. - A recipe that declares `detection` must name something in scope to detect. `ousast repos` does not trust a recipe either: against a materialised checkout it checks HEAD against the pin, that the licence file is there, and that each declared function appears above its declared line. That earned its place immediately -- the first libpng recipe named LICENSE.md, which is the filename at HEAD today and not at the pinned commit, where it is still LICENSE. Two checkouts, verified against the real repositories: - vampi (MIT, 520 lines) is the DETECTION checkout. Excerpts of it are already in the pair corpus, including the update_password miss diagnosed earlier, so a whole-repository run says directly whether a verdict survives the move off the excerpt. Its routes register through an OpenAPI operationId, so it exercises registration-based entry-point mapping rather than the decorator path. - libpng (libpng-2.0, 91k lines of C) is the ENVELOPE checkout, pinned at the parent of the commit that added the missing clamp. Its CVE is marked out of scope: an out-of-bounds read is not a family this engine arbitrates, and recording "not found" against it as a miss is the easiest way to make a measurement lie. Nothing is vendored; the recipe records the licence so an artifact can say what it ran against. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG |