From 96e7476c6d073413f4354cdd07ed4d2248a6c528 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:24:37 +0200 Subject: [PATCH] feat: the first real CVE, found in production plugin code CVE-2023-23488 is detected at classes/class.memberorder.php:936:getMemberOrderByCode -- the exact line verified in the source, in unmodified Paid Memberships Pro 2.9.7. A two-file slice carrying both ends of the CVE, 2885 lines, loads and queries in 122 seconds and reports 16 entailed injection findings. **The other fifteen are not noise.** The file holds 26 `$wpdb` query calls and **zero** of them use `->prepare`; all 26 interpolate or concatenate a variable. The engine reports 16 of 26, so it under-reports rather than over-reports, and every sampled finding is a genuine unprepared query -- `get_row("... WHERE id = '$id'")`, `get_var("... WHERE ID = '" . $this->user_id . "'")`. CVE-2023-23488 is simply the instance that got an identifier, because it is reachable unauthenticated; the rest are the same defect with different reachability. That answers the question that prompted this: whether the system can find one real CVE. It can, in production code, at the exact line, in two minutes, for a cent. What it could not do was survive its own plumbing at 637 files -- four separate quiet failures on this one checkout, all between the scan and the arbiter, none of them in the analysis. What this does NOT settle is recorded with it: the full checkout still does not load and that boundary is unmeasured; whether php2cpg resolves `new X()` and `$this->method()` is still unknown, since this slice reached the sink through a same-function flow rather than across the object; and one CVE on one plugin is not the class-by-class table over several plugins that 5.8 asks for. On cutting the input with a model, proposed when size looked like the blocker: the evidence argues for scoping rather than triage. Two of 637 files found the CVE, task 2.10 already scopes by what a project declares it ships, and diff-scoping is this spec's thesis. A model choosing what to cut adds a fifth quiet-failure surface -- drop the file holding the bug and the scan reports clean with nothing recording why, which is the shape of all four failures found today. The reverse direction is where it fits: ranking which regions get the budget, not choosing what to remove. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG --- .kiro/specs/contributor-scan/tasks.md | 8 ++++ .../2026-09-09-pmpro-cve-found.json | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 benchmarks/measurements/2026-09-09-pmpro-cve-found.json diff --git a/.kiro/specs/contributor-scan/tasks.md b/.kiro/specs/contributor-scan/tasks.md index ab04cee..bfdf73d 100644 --- a/.kiro/specs/contributor-scan/tasks.md +++ b/.kiro/specs/contributor-scan/tasks.md @@ -541,6 +541,14 @@ - Until this is answered, PHP detection is demonstrated only on files small enough to build cleanly, and no claim about WordPress can be made either way. - Observable: the largest PHP tree that loads and queries, measured, with the failure boundary recorded. + - **Partly answered 2026-09-09, and it produced the first real CVE.** A two-file slice carrying both ends + of the CVE -- `classes/class.memberorder.php` and `includes/rest-api.php`, 2885 lines -- loads, queries + in 122 seconds, and reports **CVE-2023-23488 at class.memberorder.php:936:getMemberOrderByCode**, the + exact verified line. 16 entailed injection findings, and the file holds 26 `$wpdb` query calls of which + **zero** use `->prepare`, so the engine under-reports rather than over-reports. See + `benchmarks/measurements/2026-09-09-pmpro-cve-found.json`. + - Still open: the 637-file boundary itself, and whether the REST handler two calls away can reach it -- + this slice found the sink through a same-function flow, not across the object. - _Requirements: 4.2, 4.3, 9.1_ - _Depends: 5.7_ diff --git a/benchmarks/measurements/2026-09-09-pmpro-cve-found.json b/benchmarks/measurements/2026-09-09-pmpro-cve-found.json new file mode 100644 index 0000000..0454bee --- /dev/null +++ b/benchmarks/measurements/2026-09-09-pmpro-cve-found.json @@ -0,0 +1,37 @@ +{ + "are_the_other_fifteen_noise": { + "answer": "No. The file is systemically unprepared.", + "counted": { + "engine_entailed": 16, + "unprepared_with_a_variable": 26, + "using_prepare": 0, + "wpdb_query_calls": 26 + }, + "reading": "Every sampled finding is a genuine unprepared query. The engine reports 16 of 26 such sites, so it UNDER-reports rather than over-reports here. CVE-2023-23488 is simply the instance that got an identifier, because it is reachable unauthenticated; the rest are the same defect with different reachability.", + "sampled": { + "1073": "get_row(\"... WHERE ID = '\" . $this->user_id . \"'\")", + "1512": "get_var(\"... WHERE code = '$code' LIMIT 1\")", + "671": "get_row(\"... WHERE id = '$id' LIMIT 1\")", + "705": "get_var(\"... WHERE ID = '\" . $this->user_id . \"'\")" + } + }, + "generated": "2026-09-09", + "on_cutting_the_input_with_a_model": "Proposed by the maintainer when size looked like the blocker. The evidence argues for scoping rather than triage: two of 637 files found the CVE in 122 seconds, and task 2.10 already scopes by what a project declares it ships while diff-scoping is this spec's actual thesis. A model deciding what to cut adds a FIFTH quiet-failure surface -- if it drops the file holding the bug, the scan reports clean and nothing records why, which is the exact shape of all four failures found today. The reverse direction is where a model fits: not choosing what to remove, but ranking which regions get the budget first.", + "reading": "CVE-2023-23488 is detected at classes/class.memberorder.php:936:getMemberOrderByCode -- the exact line verified in the source, in unmodified plugin code. The engine works; what has been unfit is the plumbing around it, which produced four separate quiet failures on this one checkout before a CPG it could load reached the arbiter.", + "scan": { + "build_seconds": 9.0, + "cve_found": "model_entailed injection class.memberorder.php:936:getMemberOrderByCode", + "entailed": 16, + "input": "two files carrying both ends of the CVE: classes/class.memberorder.php and includes/rest-api.php, 2885 lines", + "query_seconds": 110.9, + "regions": 57, + "total_seconds": 122.0 + }, + "spec": "contributor-scan 5.10 -- the first real CVE found in production code", + "what_it_does_not_settle": [ + "The full 637-file checkout still does not load, and that boundary is unmeasured.", + "Whether php2cpg resolves `new X()` and `$this->method()` is still unknown -- this slice found the sink through a same-function flow, not through the REST handler two calls away.", + "One CVE on one plugin. The class-by-class table over several plugins (5.8) is still the artifact worth teaching from." + ], + "what_this_settles": "The question was whether the system can find one real CVE. It can, in production code, at the exact line, in two minutes, for a cent. What it could not do was survive its own plumbing at 637 files." +}