mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
6365c49849a01f8cbce5db91e828114d538eadf8
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3b6e642433 |
fix(scan): prune a tier-zero question unless ITS partition is the incomplete one
The mechanism already existed. A question whose evidence vector says no sink of its family is in reach cannot produce a flow, and tier-zero pruning defers it. That was switched off for the whole scan whenever any of twelve degradation reasons appeared, and a multi-language repository always carries two of them, so the measured plugin arbitrated every question it could ask. Third instance this week of one blast radius, and the invariant written into the design on Monday named it. A prune blocker that names a partition now withholds pruning from that partition questions and from nobody else. cross_partition_semantics_unresolved leaves the blocker set entirely, for the reason it left the integrity gaps: a PHP family sink is PHP, so a second language cannot be why its vector saw none. Measured on Paid Memberships Pro. At 25 regions arbitration falls from 127 questions to 34 with 93 recorded deferred as tier_zero, and 19 findings before and after, identical family by family. At 500 it falls from 2,526 to 406 with 2,120 deferred. It is not sufficient, and that is the useful half. The 500-region run still fails in the same way at the same cost, because 406 questions at roughly one and a half seconds each still exceed the 300-second per-batch ceiling. This lever moved the volume; the ceiling is a separate limit. The census now records what a scan skipped and why, per family, so a cheaper scan and a narrower one stop looking identical: a family whose every question was pruned reports no sink of this family in reach rather than region offered, no question asked. 1,420 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
d105d2f84a |
fix(scan): carry the partition in both census passes, and PHP completes 120 of 127
The scoping fix looked applied and was not. The same census gap is raised in TWO passes, arbitration and evidence, and only the first carried the partition it came from, so an unscoped copy kept demoting every question in the repository. One key in one dict. What found it was the record, not the code: the census now prints each outcome reason rather than only its status, and three separate causes had been looking identical as a bare count of unresolved. Measured on Paid Memberships Pro at 25 regions: questions completed goes from 0 to 120 of 127, with its 19 findings unchanged. The seven still unresolved are the JavaScript partition own questions, which is correct, because the four missing files are its files and its answers are the ones that could be wrong. Still open, and recorded rather than implied: the push policy _blocking_degradations exempts declared exclusions but does not yet respect a partition, and a post-hoc diagnostic copy carries no language, so a JavaScript gap would still block admission of a PHP finding. 1,419 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
c93669c34f |
fix(scan): a language boundary is a declared limit, not a graph-integrity failure
Membership of GRAPH_INTEGRITY_GAPS should turn on whether a gap can make a reported answer WRONG, not on whether it cost coverage. An unparsed file may hold the sanitizer that would have cleared a flow this scan reports, so it belongs there. A flow this tool cannot follow from one language into another can hide a finding and never invent one, because a PHP sink is sanitised in PHP. cross_partition_semantics_unresolved therefore moves to DECLARED_EXCLUSION_GAPS. It touches exactly two places, both of which already honour the distinction: the outcome demotion in scan.py, and _blocking_degradations in the push policy, whose docstring already says a declared exclusion is not one of them. The reason stays published in the scope unresolved boundaries either way, so the limit is still reported; the class decides only whether every answer in the repository is demoted. This corrects the task note written earlier today, which said not to reclassify on the grounds that it would claim cross-language flows are out of scope rather than unresolved. That was a misreading of the class. PMPro still completes none of its questions, and the measurement now names why: the JavaScript partition expected 36 files and holds 32, so partition_file_census_incomplete fires and is still an integrity gap. The four absent files are js/blocks/blocks.build.js, js/jsapi.js, js/select2.min.js and webpack.config.js -- a build output, a source file, a minified third-party library and a build config. A bundled select2 costs 4,425 PHP regions their admissibility. Same over-broad blast radius, third instance of one principle in this thread, and the fix direction is recorded. The census harness now records each degradation whole rather than only its reason, because a partition census that reports files missing carries WHICH files and without them the gap is a label rather than a lead. 1,417 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
1f51c82b51 |
fix(census): stop hiding findings behind a question-outcome count, and correct the PHP record
PHP is not silent. Paid Memberships Pro establishes findings, including a true positive, and this project's own instrument is why it looked otherwise. The census harness ordered its verdict on completion before findings, so a family with eighteen findings and no completed question reported "asked but no question completed". Findings are now checked first: a question-outcome count is bookkeeping and cannot hide the thing being censused. NodeGoat and VAmPI were never affected, because they complete every question and their verdicts reached the findings check. The scan summary now also carries the total findings and the outcome status counts, so the two numbers can be seen disagreeing instead of one silently winning. The second mistake was mine rather than the harness's: only the 500-region runs had been looked at, which are the ones that time out. What PHP actually detects on PMPro. At 25 regions, 19 findings in seven minutes with no timeout; at 100 regions, 43 findings in twelve minutes with no timeout. Among them adminpages/ discountcodes.php:749, where sanitize_text_field($_REQUEST["s"]) reaches $wpdb->get_results($sqlQuery, OBJECT) through `$sqlQuery .= "WHERE code LIKE '%$s%' "` with no prepare. That is the class of CVE-2023-23488, which php.toml's own comment names as the reason those WordPress sanitizers are kept out of the sanitizer list. The prediction and the measurement agree. The precision cost beside it is measured too: output encoding reports eighteen findings at 25 regions and the three sampled are all properly escaped, two with esc_attr and one coerced with intval. Those escapers are absent from a sanitizer list that is flat across families, because none of them stops SQL injection. Keeping the injection finding costs this family its precision. The scale limit is roughly one to two seconds per question. 25 and 100 regions complete cleanly; 500 regions asks 2,526 questions, needs more than the 1,800-second ceiling and is killed. Four things are measured NOT to be the problem: the malformed-CPG bug (fixed in 4.0.625, verified on the real trigger file), the dataflow overlay (42 s on the full 637-file graph), the engine version (4.0.625 and 4.0.631 indistinguishable), and the bundled Stripe, Braintree and Twocheckout SDKs under includes/lib that the vendored list does not cover -- that was my hypothesis for the cost and the overlay measures 42 s with them and 37 s without, so it is refuted. What remains is bookkeeping, not detection: questions_completed is zero in every PHP run including the ones that find things, with cross_partition_semantics_unresolved and partition_file_census_incomplete present throughout. The admission policy reads those outcomes. 1,417 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
4e2619e248 |
feat(pre-push-safety-net): census the declared families and fix a sink matched in a comment
Seven vulnerability families are declared and two had been proven. The census asks each of them, on whole checkouts of NodeGoat and VAmPI, whether it establishes anything at all, and reports a family that answered and found nothing separately from one that was never asked. Four establish findings: injection, access_control, config_secrets and untrusted_destination. Output encoding, path, prototype and deserialization establish nothing on either subject. All 299 NodeGoat and 35 VAmPI questions completed, so the silences are answers. Three of those silences are diagnosed gaps, not clean code. NodeGoat's documented SSRF calls needle.get, which the sink vocabulary does not hold. Its XSS is server-rendered, and output encoding declares only DOM sinks, so no question it can ask could have found it. Its two access-control defects raise no obligation at all: measured with the real dominance query, every modelled data operation lives in app/data/*-dao.js and none in a route handler file, while the family scopes operations to the region's own file. Tasks 14.2 to 14.5 carry each gap; none is fixed here. The census also found a false positive and it is fixed. NodeGoat's only prototype report was the word `set` inside `// set these up in case we have an error case`: the matched node is the <operator>.assignment holding the entire 1,000-character signup handler, and sinkMatches searched a node's whole text. It now confines the text clause to the callee and excludes operator nodes. Fourth instance of this bug class after fgets/gets, resolve/resolveUrl and user/users. Measured on NodeGoat's non-vendor calls, injection sinks fall 28 to 3, prototype 13 to 2 and untrusted destination 26 to 7, with no true positive lost; req.query had been an injection sink because `query` is word-bounded inside it. Witnesses now name the sink call rather than the assignment that contained it. 1,411 tests pass, ten skipped; the six Joern-gated query tests pass in the image, including a new one that fails on the shape this fixed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |