From 8579c10e7c5a7d5b55f883ff98b98e79a927491c Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:43:10 +0200 Subject: [PATCH] docs: the WordPress checkout scans, and does not find its CVE Reporting the result rather than the intention. Paid Memberships Pro 2.9.7 with CVE-2023-23488 is pinned, verified in the code, and scans cleanly: 637 files, 117k lines, 1577 regions, build 47s, query 74s, 140s total, no degradations. **The CVE is not detected.** Both ends are inside the budget -- the REST handler at position 290 as a recognised entry point, the `$wpdb->get_var` sink at 472 -- and the taint query runs for 23 seconds. So the gap is the middle: the flow crosses `new MemberOrder( $code )` into a constructor and then `$this->getMemberOrderByCode( $code )`, both dynamic in PHP. Whether php2cpg resolves either into a callee edge is UNVERIFIED; two attempts to query the call graph failed on script errors and were abandoned rather than guessed at. Recorded as 5.7, with the answer required before anything is concluded about the fact tables. Three engine defects had to be fixed before the scan could produce even this result, and each had previously reported the plugin clean for a different reason: 1577 regions, 0 scanned joern-parse failed where php2cpg succeeded 500 scanned, taint 0.05s a 1.49MB batch over Linux's 128KB argv cap 500 scanned, taint 21.7s the entry point was not recognised as one The middle one is the one to remember. Every honesty mechanism in this codebase labels a verdict -- suspicion, regions_unjudged, regions_unasked, stated family limits -- and there no verdict was produced to label, so a vulnerable plugin was reported clean with every counter agreeing. What is established: the WordPress engine path works end to end. What is not: detection on a real plugin. One checkout that does not find its CVE is not yet evidence either way about the facts, and 5.7 has to answer whether the call graph can carry the flow at all. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG --- .kiro/specs/contributor-scan/tasks.md | 17 ++++++ .../measurements/2026-09-09-repo-pmpro.json | 52 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 benchmarks/measurements/2026-09-09-repo-pmpro.json diff --git a/.kiro/specs/contributor-scan/tasks.md b/.kiro/specs/contributor-scan/tasks.md index 21d65d9..08a3c63 100644 --- a/.kiro/specs/contributor-scan/tasks.md +++ b/.kiro/specs/contributor-scan/tasks.md @@ -476,6 +476,23 @@ - _Requirements: 8.1, 9.1_ - _Depends: 5.1_ +- [ ] 5.7 Does the PHP call graph carry a flow through an object? + - **The WordPress checkout scans and does not find its CVE.** Both ends are inside the budget -- + `pmpro_rest_api_get_order` at position 290 as a recognised entry point, `getMemberOrderByCode` at 472 -- + and the taint query runs for 23 seconds. So the question is the middle. + - The flow crosses `new MemberOrder( $code )` into a constructor and then + `$this->getMemberOrderByCode( $code )`. Both are dynamic in PHP, and whether php2cpg resolves either into + a CALLEE edge is unverified: two attempts to query the call graph failed on script errors and were + abandoned rather than guessed at. + - Answer that first. If the edges exist, the gap is elsewhere -- the source may be `$request->get_params()` + rather than a superglobal, which `parameter_sources` should cover but has not been checked on this shape. + If they do not, task 2.4's interprocedural region cannot bridge an object boundary and PHP needs + something else, which is a finding worth having either way. + - Observable: a recorded answer naming which edges exist, and either the CVE found or a stated reason it + cannot be. + - _Requirements: 4.4, 8.1, 9.1_ + - _Depends: 5.1_ + ## Group 6 — Regression baselines - [ ] 6.1 Repository baseline and delta diff --git a/benchmarks/measurements/2026-09-09-repo-pmpro.json b/benchmarks/measurements/2026-09-09-repo-pmpro.json new file mode 100644 index 0000000..aa03857 --- /dev/null +++ b/benchmarks/measurements/2026-09-09-repo-pmpro.json @@ -0,0 +1,52 @@ +{ + "detection": { + "both_in_budget": true, + "cve": "CVE-2023-23488", + "entry_point": "includes/rest-api.php:pmpro_rest_api_get_order -- recognised, rank 0.80, position 290, entry_point", + "found": false, + "open_question": "The flow crosses `new MemberOrder( $code )` into a constructor and then `$this->getMemberOrderByCode( $code )`. Whether Joern's PHP frontend resolves either into a call edge is unverified -- two attempts to query the call graph failed on script errors. If it does not, task 2.4's interprocedural region cannot bridge this and the PHP story needs something else. Recorded as 5.7.", + "sink": "classes/class.memberorder.php:936 getMemberOrderByCode -- $wpdb->get_var with concatenated $code, position 472" + }, + "envelope": { + "build_seconds": 46.9, + "files": 637, + "lines": 117302, + "note": "the 500 is the ScanBudget max_regions cap, not a failure; both the CVE's entry point (position 290) and its sink (position 472) are inside it", + "per_kind": { + "config": 22.0, + "dominance": 28.7, + "taint": 23.1 + }, + "query_seconds": 73.8, + "regions": 1577, + "regions_scanned": 500, + "regions_unjudged": 1100, + "total_seconds": 140.1 + }, + "generated": "2026-09-09", + "reading": "The checkout is real, the CVE is verified in the code, and the scan runs cleanly on 117k lines of PHP. **The CVE is not detected.** That is the result, and three separate engine defects had to be fixed before the scan could even produce it -- each of which had previously reported the plugin clean for a different reason.", + "repo": "Paid Memberships Pro 2.9.7 at 1a900be3f09f243876150fc3c04a97bcb24ef84b, CVE-2023-23488", + "spec": "contributor-scan 5.1 -- the WordPress detection checkout", + "three_defects_found_getting_here": [ + { + "cause": "joern-parse threw after applying overlays; php2cpg on the identical tree produced a 4.3MB CPG with no errors", + "fix": "retry through 2cpg, which is what Joern's own message recommends for a large codebase", + "id": "joern-parse-fails-where-the-frontend-succeeds", + "symptom": "1577 regions, 0 scanned, cpg_build_failed in 42s" + }, + { + "cause": "Linux caps a single argument at MAX_ARG_STRLEN (128KB); the taint batch was 2500 requests and 1.49MB, execve returned E2BIG, and query_batch's {} read as 'no rows found'", + "fix": "stage the payload to a file and pass the path", + "id": "a-batch-larger-than-the-argv-limit", + "symptom": "500 scanned, 0 found, taint query 0.05 seconds -- less than a JVM takes to start", + "why_it_matters": "this is the failure the rung ladder cannot catch -- every other honesty counter labels a VERDICT, and here none was produced to label" + }, + { + "cause": "PMPro registers routes as array( $this, 'method' ) and the recogniser matched only string callbacks, so the entry point ranked 0.30 at position 1036, below the budget. The sink at position 450 WAS scanned and found nothing, because only an entry point's parameters are untrusted.", + "fix": "recognise array callables; the handler moves to position 290 at rank 0.80", + "id": "an-array-callable-is-not-a-handler", + "symptom": "500 scanned, 0 found, taint query 21.66s -- the query ran and found nothing" + } + ], + "what_this_says_about_the_v0_1_target": "The WordPress engine path works: php2cpg parses, the facts derive, regions are function-scoped, hooks and REST routes carry declared access, and a 117k-line plugin scans in 140 seconds. What is not established is detection on a real plugin, and one checkout that does not find its CVE is not yet evidence either way about the fact tables -- 5.7 has to answer whether the call graph can carry the flow at all." +}