feat(ruleset): PHP quick-mode rules from the engine's vocabulary, measured on the development corpus

Quick mode had no PHP rules, so a PHP quick scan read the files and reported a plausible zero.
ruleset/php/rules.toml adds 13 line/statement rules built on semantic/php.toml's sinks, sources and
sanitizers (sql/wpdb, command, code_eval, unserialize, echo, file_inclusion, filesystem,
outbound_request, redirect). 9 are enabled and 4 are shadow (sql-call-composition, code-eval,
unserialize, xss-echo-input). A rule is shadow when it fired on the corpus but never at a known site.
precision_estimate is the measured lower bound.

Measured in-sample on the development corpus only (pmpro introduce/repair/benign, population-v1 PHP
cases; population-v2 was not read). Known site hit on the vulnerable pin in 4 of 5 cases (LearnPress
missed). The hit disappears on the fixed pin for pmpro and YesWiki. Numbers, per-pin files/bytes read
and the engine comparison are in benchmarks/measurements/2026-09-30-php-quick-rules/.

- fixtures php-vulnerable / php-benign plus manifests: ousast benchmark covers PHP
  (9/14 reported, 13/14 including shadow; the benign twin records 2 known false alerts)
- PHP `#` comments are comments; `#[...]` attributes are not
- the alerts task now reports PHP as quick coverage; alerts-engine keeps running the engine for PHP
  next to the quick rules (ENGINE_ALONGSIDE_QUICK)
- other languages' benchmark findings and metrics are unchanged

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-30 15:05:47 +02:00
co-authored by Claude Opus 5.5
parent 45d02ecf20
commit 7927d6e539
16 changed files with 1757 additions and 17 deletions
+89
View File
@@ -0,0 +1,89 @@
<?php
// Benign twin of benchmarks/fixtures/php-vulnerable/app.php: the same handlers, each one repaired the way its
// upstream fix did it. Every quick-mode finding here is a false alert (benchmarks/manifests/php-benign.toml).
$conn = mysqli_connect('localhost', 'app', 'app', 'app');
function order_by_code($code)
{
global $wpdb;
return $wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}orders WHERE code = %s LIMIT 1", $code));
}
function find_user($conn)
{
$stmt = mysqli_prepare($conn, 'SELECT * FROM users WHERE name = ?');
mysqli_stmt_bind_param($stmt, 's', $_GET['name']);
mysqli_stmt_execute($stmt);
return mysqli_stmt_get_result($stmt);
}
function members_sorted($order)
{
$order = in_array(strtoupper($order), array('ASC', 'DESC'), true) ? strtoupper($order) : 'ASC';
return ' ORDER BY u.user_login ' . ('DESC' === $order ? 'DESC' : 'ASC') . ' ';
}
function create_form($data)
{
return 'INSERT INTO forms (id, label) VALUES (' . intval($data['id']) . ', "x")';
}
function captcha_cleanup($db, $userAgent)
{
$delete = sprintf(
"DELETE FROM captcha WHERE useragent = '%s'",
$db->escape($userAgent)
);
$db->query($delete);
}
function ping()
{
system('ping -c 1 ' . escapeshellarg($_GET['host']));
}
function calculate($formula)
{
return is_numeric($formula) ? (float) $formula : 0.0;
}
function restore_session()
{
return json_decode($_COOKIE['session'], true);
}
function greet()
{
echo 'Hello ' . htmlspecialchars($_GET['name'], ENT_QUOTES);
}
function page()
{
$pages = array('home', 'about');
include in_array($_GET['page'], $pages, true) ? $_GET['page'] . '.php' : 'home.php';
}
function download()
{
readfile('/var/www/files/' . basename($_GET['file']));
}
function fetch_preview()
{
$url = filter_var($_GET['url'], FILTER_VALIDATE_URL);
$ch = curl_init(wp_http_validate_url($url) ? $url : 'about:blank');
return curl_exec($ch);
}
function go_back()
{
wp_safe_redirect(wp_validate_redirect(wp_unslash($_GET['next']), home_url()));
}
function find_order($conn)
{
$id = intval($_POST['id']);
$sql = 'SELECT * FROM orders WHERE id = ' . $id;
return mysqli_query($conn, $sql);
}
@@ -0,0 +1,87 @@
<?php
// Intentionally vulnerable PHP fixture for the quick-mode PHP rules. Do not deploy.
// Each handler has one labeled weakness (benchmarks/manifests/php-vulnerable.toml); the fixed twin is
// benchmarks/fixtures/php-benign/app.php.
$conn = mysqli_connect('localhost', 'app', 'app', 'app');
function order_by_code($code)
{
global $wpdb;
return $wpdb->get_var("SELECT id FROM {$wpdb->prefix}orders WHERE code = '" . $code . "' LIMIT 1");
}
function find_user($conn)
{
return mysqli_query($conn, "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'");
}
function members_sorted($order)
{
$order = esc_sql($order); // escaping does not protect an unquoted ORDER BY
return " ORDER BY u.user_login {$order} ";
}
function create_form($data)
{
return 'INSERT INTO forms (id, label) VALUES (' . $data['id'] . ', "x")';
}
function captcha_cleanup($db, $userAgent)
{
$delete = sprintf(
"DELETE FROM captcha WHERE useragent = '%s'",
$userAgent
);
$db->query($delete);
}
function ping()
{
system('ping -c 1 ' . $_GET['host']);
}
function calculate($formula)
{
eval('$value = ' . $formula . ';');
return $value;
}
function restore_session()
{
return unserialize($_COOKIE['session']);
}
function greet()
{
echo 'Hello ' . $_GET['name'];
}
function page()
{
include $_GET['page'] . '.php';
}
function download()
{
readfile('/var/www/files/' . $_GET['file']);
}
function fetch_preview()
{
$ch = curl_init($_GET['url']);
return curl_exec($ch);
}
function go_back()
{
header('Location: ' . $_GET['next']);
}
// Not visible to a line rule: the value reaches the sink through a variable on another line.
function find_order($conn)
{
$id = $_POST['id'];
$sql = "SELECT * FROM orders WHERE id = '" . $id . "'";
return mysqli_query($conn, $sql);
}
@@ -0,0 +1,65 @@
<?php
// Safe equivalents. None of these lines should produce a finding.
function order_by_code_safe($code)
{
global $wpdb;
return $wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}orders WHERE code = %s LIMIT 1", $code));
}
function orders_in_safe($ids)
{
global $wpdb;
$ids_format = implode(', ', array_fill(0, count($ids), '%d'));
return $wpdb->prepare('SELECT * FROM orders WHERE id IN (' . $ids_format . ')', $ids);
}
function page_limit_safe($page)
{
return ' LIMIT ' . intval($page) . ', 20';
}
function quoted_escaped_safe($name)
{
return "SELECT * FROM users WHERE name = '" . esc_sql($name) . "'";
}
function ping_safe()
{
system('ping -c 1 ' . escapeshellarg($_GET['host']));
}
function version_safe()
{
exec('git describe --tags', $output);
return $output;
}
function greet_safe()
{
echo 'Hello ' . htmlspecialchars($_GET['name'], ENT_QUOTES);
}
function download_safe()
{
readfile('/var/www/files/' . basename($_GET['file']));
}
function restore_safe($raw)
{
return unserialize($raw, ['allowed_classes' => false]);
}
function query_args_safe()
{
$args = [];
$args['include'] = [(int) $_REQUEST['p']];
return $args;
}
function comment_only()
{
// system('rm -rf ' . $_GET['dir']) would be dangerous but this is a comment
# eval($_POST['code']) is a comment too
return 'ok';
}
+12
View File
@@ -0,0 +1,12 @@
name = "php-benign"
# The repaired twin of php-vulnerable: no expected findings, so every quick-mode finding is a false alert.
language = "php_web"
frameworks = ["wordpress"]
setup = []
modes = ["quick", "standard"]
known_noise = []
baselines = []
[source]
type = "local"
path = "../fixtures/php-benign"
+136
View File
@@ -0,0 +1,136 @@
name = "php-vulnerable"
language = "php_web"
frameworks = ["wordpress"]
setup = []
modes = ["quick", "standard"]
known_noise = []
baselines = []
[source]
type = "local"
path = "../fixtures/php-vulnerable"
[[expected]]
cwe = "CWE-89"
class = "sql injection"
path = "app.php"
line = 11
rule_id = "php-wpdb-sql-composition"
sink = "get_var"
evidence = "wpdb get_var query concatenates the order code without prepare"
[[expected]]
cwe = "CWE-89"
class = "sql injection"
path = "app.php"
line = 16
rule_id = "php-sql-call-composition"
sink = "mysqli_query"
evidence = "mysqli_query composes the query from request input"
[[expected]]
cwe = "CWE-89"
class = "sql injection"
path = "app.php"
line = 22
rule_id = "php-sql-unquoted-interpolation"
sink = "ORDER"
evidence = "esc_sql value interpolated into an unquoted ORDER BY"
[[expected]]
cwe = "CWE-89"
class = "sql injection"
path = "app.php"
line = 27
rule_id = "php-sql-unquoted-concat"
sink = "VALUES"
evidence = "form id concatenated unquoted into VALUES"
[[expected]]
cwe = "CWE-89"
class = "sql injection"
path = "app.php"
line = 32
rule_id = "php-sql-sprintf-unescaped"
sink = "sprintf"
evidence = "sprintf SQL with an unescaped quoted %s user agent"
[[expected]]
cwe = "CWE-78"
class = "command injection"
path = "app.php"
line = 41
rule_id = "php-command-injection"
sink = "system"
evidence = "system runs a command built from request input"
[[expected]]
cwe = "CWE-95"
class = "code injection"
path = "app.php"
line = 46
rule_id = "php-code-eval"
sink = "eval"
evidence = "eval executes a formula string"
[[expected]]
cwe = "CWE-502"
class = "insecure deserialization"
path = "app.php"
line = 52
rule_id = "php-unserialize"
sink = "unserialize"
evidence = "unserialize of a cookie"
[[expected]]
cwe = "CWE-79"
class = "cross-site scripting"
path = "app.php"
line = 57
rule_id = "php-xss-echo-input"
sink = "echo"
evidence = "request input echoed without escaping"
[[expected]]
cwe = "CWE-98"
class = "file inclusion"
path = "app.php"
line = 62
rule_id = "php-file-inclusion-input"
sink = "include"
evidence = "include of a request-chosen path"
[[expected]]
cwe = "CWE-22"
class = "path traversal"
path = "app.php"
line = 67
rule_id = "php-path-input"
sink = "readfile"
evidence = "readfile of a request-chosen path"
[[expected]]
cwe = "CWE-918"
class = "server-side request forgery"
path = "app.php"
line = 72
rule_id = "php-ssrf-input"
sink = "curl_init"
evidence = "curl request to a request-chosen URL"
[[expected]]
cwe = "CWE-601"
class = "open redirect"
path = "app.php"
line = 78
rule_id = "php-open-redirect"
sink = "header"
evidence = "Location header from request input"
[[expected]]
cwe = "CWE-89"
class = "sql injection"
path = "app.php"
line = 86
sink = "mysqli_query"
evidence = "query built from a request value on an earlier line (not visible to a line rule)"
@@ -0,0 +1,181 @@
"""Measure the PHP quick-mode rules on the PHP development corpus (never population-v2).
Pins: pmpro vulnerable / fixed / benign (benchmarks/push/inputs.json, role development) and the spent
population-v1 PHP cases' vulnerable and fixed pins, exported beforehand with benchmarks/independent/evaluate.py's
`export` into <corpus>/checkouts/<case>/<label> (pmpro from the repo cache, benign = vulnerable + the declared
authored edit).
Known sites: pmpro's is the declared CVE-2023-23488 function (getMemberOrderByCode) on each pin; a v1 case's are
its fix hunks (vulnerable side on the vulnerable pin, fixed side on the fixed pin; test paths dropped) widened by
evaluate.LINE_WINDOW, plus the declared sink line where the fix does not touch the sink (LearnPress).
The instrument must have read its input: every pin reports the PHP files and bytes the scan read, and a pin with
zero PHP bytes fails the run.
Usage: python measure_php_quick.py <corpus dir> <out.json>
"""
from __future__ import annotations
import json
import sys
import time
from collections import Counter, defaultdict
from pathlib import Path
sys.path.insert(0, "benchmarks/independent")
import evaluate # noqa: E402
from openultrasast.findings import quick_scan_findings # noqa: E402
from openultrasast.mapping import analyze_entry_points, attach_reachability_hints # noqa: E402
from openultrasast.preprocess import preprocess_repository # noqa: E402
from openultrasast.rank import rank_targets # noqa: E402
V1 = ("wp-ultimate-member-sqli", "wp-learnpress-sqli", "phpmyfaq-captcha-ua-sqli", "yeswiki-bazar-form-sqli")
DECLARED = {"wp-learnpress-sqli": ("inc/Databases/class-lp-db.php", "$FIELDS = implode( ',', array_unique( $filter->only_fields ) );")}
W = evaluate.LINE_WINDOW
def scan(root: Path) -> tuple[list, dict]:
_, targets = preprocess_repository(root)
php = [t for t in targets if t.language == "php"]
read = {"php_files": len(php), "php_bytes": sum(len((root / t.path).read_bytes()) for t in php)}
if not read["php_bytes"]:
raise SystemExit(f"instrument failure: no PHP bytes read under {root}")
targets = attach_reachability_hints(targets, analyze_entry_points(root, targets))
started = time.monotonic()
findings = [f for f in quick_scan_findings(root, targets, rank_targets(targets)) if f.finding_id.startswith("php-")]
read["scan_seconds"] = round(time.monotonic() - started, 1)
return findings, read
def function_range(path: Path, name: str) -> tuple[int, int]:
lines = path.read_text(errors="replace").splitlines()
start = next(i for i, line in enumerate(lines, 1) if f"function {name}(" in line)
end = next((i for i, line in enumerate(lines[start:], start + 1) if "function " in line), len(lines))
return start, end - 1
def line_of(path: Path, needle: str) -> int:
return next(i for i, line in enumerate(path.read_text(errors="replace").splitlines(), 1) if needle in line)
def at(finding, sites: dict[str, list[tuple[int, int]]]) -> bool:
return any(first <= (finding.line or -1) <= last for first, last in sites.get(finding.path, []))
def summarize(findings: list, sites: dict[str, list[tuple[int, int]]]) -> dict:
hits = [f for f in findings if at(f, sites)]
return {
"findings": len(findings),
"by_rule": dict(sorted(Counter(f.finding_id.split(":", 1)[0] for f in findings).items())),
"by_rule_enabled": len([f for f in findings if f.status == "enabled"]),
"at_known_site": [f.finding_id for f in hits],
}
def main() -> int:
corpus, out = Path(sys.argv[1]), Path(sys.argv[2])
checkouts = corpus / "checkouts"
result: dict = {"window": W, "cases": {}}
# pmpro: the declared CVE function on each pin.
pmpro_dirs = {
"vulnerable": Path.home() / ".cache/openultrasast/repos/pmpro/1a900be3f09f",
"fixed": Path.home() / ".cache/openultrasast/repos/pmpro/2f65530f9442",
"benign": checkouts / "pmpro" / "benign",
}
pmpro: dict = {}
for label, root in pmpro_dirs.items():
findings, read = scan(root)
rel = "classes/class.memberorder.php"
first, last = function_range(root / rel, "getMemberOrderByCode")
sites = {rel: [(first, last)]}
pmpro[label] = {"root": str(root), "read": read, "known_site": f"{rel}:{first}-{last}", **summarize(findings, sites)}
pmpro[label]["_ids"] = sorted(f.finding_id for f in findings)
print("pmpro", label, read, pmpro[label]["findings"], pmpro[label]["at_known_site"], file=sys.stderr)
result["cases"]["pmpro"] = pmpro
for case in evaluate.cases():
if case["id"] not in V1:
continue
entry: dict = {}
for label, side in (("vulnerable", "old"), ("fixed", "new")):
root = checkouts / case["id"] / label
findings, read = scan(root)
sites = {
path: [(a - W, b + W) for a, b in ranges]
for path, ranges in evaluate.hunks(case, side).items()
if path.endswith(".php") and not path.startswith(("tests/", "test/")) and "/tests/" not in path
}
if case["id"] in DECLARED:
rel, needle = DECLARED[case["id"]]
n = line_of(root / rel, needle)
sites.setdefault(rel, []).append((n - W, n + W))
entry[label] = {"read": read, "known_sites": {p: r for p, r in sorted(sites.items())}, **summarize(findings, sites)}
entry[label]["_ids"] = sorted(f.finding_id for f in findings)
print(case["id"], label, read, entry[label]["findings"], entry[label]["at_known_site"], file=sys.stderr)
result["cases"][case["id"]] = entry
result["fixtures"] = fixtures()
result["rules"] = per_rule(result)
for pins in result["cases"].values():
for pin in pins.values():
pin.pop("_ids")
out.write_text(json.dumps(result, indent=1, sort_keys=True) + "\n")
return 0
def fixtures() -> dict:
"""The committed fixture benchmarks, scored exactly as `ousast benchmark` scores them."""
from openultrasast.benchmark import BenchmarkRun, evaluate_benchmark, load_benchmark_manifest, resolve_benchmark_source
from openultrasast.gate import _quick_scan
result = {}
for name in ("php-vulnerable", "php-benign"):
path = Path("benchmarks/manifests") / f"{name}.toml"
manifest = load_benchmark_manifest(path)
target = resolve_benchmark_source(path, manifest)
findings = _quick_scan(target)
run = BenchmarkRun(benchmark_run_id="php-quick", root=Path("/tmp/php-quick"), manifest=manifest)
bench = evaluate_benchmark(run=run, mode="quick", findings=findings, scan_id=None, scan_run_dir=None)
m = bench.metrics
result[name] = {
"expected": m.expected_findings_total, "matched": m.matched_findings_total, "findings": m.actual_findings_total,
"false_positives": [fp.finding_id for fp in bench.false_positives], "missed": [x.rule_id or x.cwe for x in bench.misses],
"true_positives": sorted({f.finding_id for f in findings} - {fp.finding_id for fp in bench.false_positives}),
} # fmt: skip
return result
def per_rule(result: dict) -> dict:
"""Firings per rule and the status/precision policy applied to them (see the module docstring of the ruleset).
enabled -- the rule hit a known vulnerable site on the corpus, or it never fired on the corpus (no false-alert
pressure measured; fixture evidence only);
shadow -- it fired on the corpus but never at a known site: every corpus firing was on code whose known bug is
elsewhere, and it fired as often on the fixed pins.
precision lower bound = (known-site hits on vulnerable pins + fixture true positives)
/ (firings on vulnerable pins + fixture firings, both twins); an unlabeled firing counts false.
"""
rows: dict = defaultdict(lambda: defaultdict(int))
for pins in result["cases"].values():
for label, pin in pins.items():
for fid in pin["_ids"]:
rows[fid.split(":", 1)[0]][label] += 1
for fid in pin["at_known_site"]:
rows[fid.split(":", 1)[0]][label + "_at_site"] += 1
for name, fixture in result["fixtures"].items():
for fid in fixture["true_positives"]:
rows[fid.split(":", 1)[0]]["fixture_tp"] += 1
for fid in fixture["false_positives"]:
rows[fid.split(":", 1)[0]]["fixture_fp"] += 1
rules = {}
for rule, row in sorted(rows.items()):
fired, at_site = row["vulnerable"], row["vulnerable_at_site"]
status = "enabled" if at_site or not fired else "shadow"
denominator = fired + row["fixture_tp"] + row["fixture_fp"]
precision = round((at_site + row["fixture_tp"]) / denominator, 3) if denominator else None
rules[rule] = {**dict(sorted(row.items())), "status": status, "precision_lower_bound": precision, "n": denominator}
return rules
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,864 @@
{
"cases": {
"phpmyfaq-captcha-ua-sqli": {
"fixed": {
"at_known_site": [
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:294",
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:320",
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:336"
],
"by_rule": {
"php-sql-sprintf-unescaped": 44,
"php-sql-unquoted-concat": 10,
"php-sql-unquoted-interpolation": 5
},
"by_rule_enabled": 59,
"findings": 59,
"known_sites": {
"phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php": [
[
267,
291
],
[
282,
302
],
[
291,
313
],
[
296,
316
],
[
304,
329
],
[
316,
336
],
[
318,
338
],
[
321,
341
],
[
334,
357
],
[
340,
360
],
[
475,
499
],
[
482,
506
]
]
},
"read": {
"php_bytes": 12893136,
"php_files": 762,
"scan_seconds": 14.4
}
},
"vulnerable": {
"at_known_site": [
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:289",
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:309",
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:325",
"php-sql-sprintf-unescaped:phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:474"
],
"by_rule": {
"php-sql-sprintf-unescaped": 45,
"php-sql-unquoted-concat": 10,
"php-sql-unquoted-interpolation": 5
},
"by_rule_enabled": 60,
"findings": 60,
"known_sites": {
"phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php": [
[
266,
286
],
[
277,
297
],
[
286,
308
],
[
291,
311
],
[
298,
318
],
[
305,
325
],
[
307,
327
],
[
310,
330
],
[
323,
346
],
[
329,
349
],
[
464,
484
],
[
466,
486
]
]
},
"read": {
"php_bytes": 12888581,
"php_files": 762,
"scan_seconds": 13.9
}
}
},
"pmpro": {
"benign": {
"at_known_site": [
"php-wpdb-sql-composition:classes/class.memberorder.php:937"
],
"by_rule": {
"php-sql-unquoted-concat": 9,
"php-sql-unquoted-interpolation": 28,
"php-wpdb-sql-composition": 55
},
"by_rule_enabled": 92,
"findings": 92,
"known_site": "classes/class.memberorder.php:934-946",
"read": {
"php_bytes": 4244314,
"php_files": 637,
"scan_seconds": 5.5
},
"root": "/tmp/claude-1000/-home-mc-Source-OpenUltraSAST/c09e30b2-39a4-4564-bc31-6ea71ffaa198/scratchpad/phpquick/corpus/checkouts/pmpro/benign"
},
"fixed": {
"at_known_site": [],
"by_rule": {
"php-sql-unquoted-concat": 6,
"php-sql-unquoted-interpolation": 21,
"php-wpdb-sql-composition": 39
},
"by_rule_enabled": 66,
"findings": 66,
"known_site": "classes/class.memberorder.php:934-946",
"read": {
"php_bytes": 4246202,
"php_files": 637,
"scan_seconds": 5.3
},
"root": "/home/mc/.cache/openultrasast/repos/pmpro/2f65530f9442"
},
"vulnerable": {
"at_known_site": [
"php-wpdb-sql-composition:classes/class.memberorder.php:936"
],
"by_rule": {
"php-sql-unquoted-concat": 9,
"php-sql-unquoted-interpolation": 28,
"php-wpdb-sql-composition": 55
},
"by_rule_enabled": 92,
"findings": 92,
"known_site": "classes/class.memberorder.php:933-945",
"read": {
"php_bytes": 4244270,
"php_files": 637,
"scan_seconds": 5.3
},
"root": "/home/mc/.cache/openultrasast/repos/pmpro/1a900be3f09f"
}
},
"wp-learnpress-sqli": {
"fixed": {
"at_known_site": [],
"by_rule": {
"php-sql-unquoted-concat": 19,
"php-sql-unquoted-interpolation": 23,
"php-unserialize": 1,
"php-wpdb-sql-composition": 4
},
"by_rule_enabled": 46,
"findings": 47,
"known_sites": {
"inc/Databases/class-lp-db.php": [
[
597,
617
]
],
"inc/Models/Courses.php": [
[
9,
29
],
[
72,
94
],
[
78,
107
]
],
"inc/class-lp-assets.php": [
[
299,
319
]
],
"inc/jwt/rest-api/version1/class-lp-rest-courses-v1-controller.php": [
[
425,
445
],
[
431,
459
],
[
448,
468
],
[
532,
552
],
[
536,
557
]
]
},
"read": {
"php_bytes": 3515681,
"php_files": 801,
"scan_seconds": 4.4
}
},
"vulnerable": {
"at_known_site": [],
"by_rule": {
"php-sql-unquoted-concat": 19,
"php-sql-unquoted-interpolation": 23,
"php-unserialize": 1,
"php-wpdb-sql-composition": 4
},
"by_rule_enabled": 46,
"findings": 47,
"known_sites": {
"inc/Databases/class-lp-db.php": [
[
597,
617
]
],
"inc/Models/Courses.php": [
[
8,
28
],
[
70,
90
],
[
73,
93
]
],
"inc/class-lp-assets.php": [
[
299,
319
]
],
"inc/jwt/rest-api/version1/class-lp-rest-courses-v1-controller.php": [
[
425,
445
],
[
430,
450
],
[
440,
462
],
[
526,
547
],
[
531,
552
]
]
},
"read": {
"php_bytes": 3515194,
"php_files": 801,
"scan_seconds": 4.7
}
}
},
"wp-ultimate-member-sqli": {
"fixed": {
"at_known_site": [
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:793",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:810",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:816",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:825",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:833",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:842",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:902",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:907"
],
"by_rule": {
"php-sql-unquoted-concat": 2,
"php-sql-unquoted-interpolation": 9,
"php-wpdb-sql-composition": 3
},
"by_rule_enabled": 14,
"findings": 14,
"known_sites": {
"includes/core/class-member-directory-meta.php": [
[
778,
799
],
[
780,
800
],
[
782,
802
],
[
798,
818
],
[
805,
825
],
[
814,
834
],
[
822,
842
],
[
831,
851
],
[
862,
882
],
[
891,
911
],
[
893,
913
],
[
896,
916
]
]
},
"read": {
"php_bytes": 2588115,
"php_files": 261,
"scan_seconds": 3.5
}
},
"vulnerable": {
"at_known_site": [
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:791",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:807",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:812",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:820",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:827",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:835",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:894",
"php-sql-unquoted-interpolation:includes/core/class-member-directory-meta.php:898"
],
"by_rule": {
"php-sql-unquoted-concat": 2,
"php-sql-unquoted-interpolation": 9,
"php-wpdb-sql-composition": 3
},
"by_rule_enabled": 14,
"findings": 14,
"known_sites": {
"includes/core/class-member-directory-meta.php": [
[
777,
797
],
[
779,
799
],
[
780,
800
],
[
795,
815
],
[
801,
821
],
[
809,
829
],
[
816,
836
],
[
824,
844
],
[
855,
875
],
[
883,
903
],
[
885,
905
],
[
887,
907
]
]
},
"read": {
"php_bytes": 2587061,
"php_files": 261,
"scan_seconds": 3.3
}
}
},
"yeswiki-bazar-form-sqli": {
"fixed": {
"at_known_site": [],
"by_rule": {
"php-code-eval": 2,
"php-sql-call-composition": 3,
"php-sql-unquoted-concat": 15,
"php-sql-unquoted-interpolation": 4,
"php-unserialize": 2,
"php-xss-echo-input": 2
},
"by_rule_enabled": 19,
"findings": 28,
"known_sites": {
"tools/bazar/controllers/FormController.php": [
[
29,
51
]
],
"tools/bazar/services/FormManager.php": [
[
248,
268
]
]
},
"read": {
"php_bytes": 3971093,
"php_files": 655,
"scan_seconds": 5.2
}
},
"vulnerable": {
"at_known_site": [
"php-sql-unquoted-concat:tools/bazar/services/FormManager.php:258"
],
"by_rule": {
"php-code-eval": 2,
"php-sql-call-composition": 3,
"php-sql-unquoted-concat": 16,
"php-sql-unquoted-interpolation": 4,
"php-unserialize": 2,
"php-xss-echo-input": 2
},
"by_rule_enabled": 20,
"findings": 29,
"known_sites": {
"tools/bazar/controllers/FormController.php": [
[
28,
48
]
],
"tools/bazar/services/FormManager.php": [
[
248,
268
]
]
},
"read": {
"php_bytes": 3970854,
"php_files": 655,
"scan_seconds": 5.1
}
}
}
},
"engine": {
"pmpro": {
"fixed": {
"at_known_site": [],
"completed": 318,
"degradations": [
"cross_partition_semantics_unresolved",
"partition_file_census_incomplete",
"process_timeout",
"query_budget_reserved"
],
"findings": 6,
"questions": 382,
"read": "read 673 files, 4603062 bytes; 4461 regions offer ('injection',)",
"seconds": 2025.8,
"sites": [
"classes/class-pmpro-discount-codes.php:266:save",
"classes/class.memberorder.php:1469:saveOrder",
"classes/class.pmproemail.php:549:sendCheckoutEmail",
"classes/class.pmproemail.php:668:sendCheckoutAdminEmail",
"includes/functions.php:3823:pmpro_insert_or_replace",
"services/ipnhandler.php:819:pmpro_ipnSaveOrder"
]
},
"source": "frozen export of 45d02ec, openultrasast:dev, --memory 3g, --network none, family injection",
"vulnerable": {
"at_known_site": [
"classes/class.memberorder.php:936:getMemberOrderByCode"
],
"completed": 125,
"degradations": [
"cross_partition_semantics_unresolved",
"partition_file_census_incomplete",
"process_timeout",
"query_budget_reserved"
],
"findings": 10,
"questions": 382,
"read": "read 673 files, 4601130 bytes; 4461 regions offer ('injection',)",
"seconds": 1993.4,
"sites": [
"classes/class.memberorder.php:1467:saveOrder",
"classes/class.memberorder.php:925:getLastMemberOrder",
"classes/class.memberorder.php:936:getMemberOrderByCode",
"classes/class.memberorder.php:984:getMemberOrderByPayPalToken",
"classes/class.pmproemail.php:550:sendCheckoutEmail",
"classes/class.pmproemail.php:661:sendCheckoutAdminEmail",
"includes/profile.php:337:pmpro_membership_level_profile_fields_update",
"includes/profile.php:344:pmpro_membership_level_profile_fields_update",
"includes/profile.php:350:pmpro_membership_level_profile_fields_update",
"services/ipnhandler.php:819:pmpro_ipnSaveOrder"
]
}
},
"v1_recorded": {
"phpmyfaq-captcha-ua-sqli": {
"detected": false,
"fixed": {
"completed": 0,
"findings": 0,
"questions": 5000,
"seconds": 426.2
},
"unanswered": true,
"vulnerable": {
"completed": 0,
"findings": 0,
"questions": 5000,
"seconds": 437.7
}
},
"source": "not re-run: the population-v1 run of 2026-09-24 (analyzer ebcc2cf), ~/ousast-results/independent-v1/score.json",
"wp-learnpress-sqli": {
"detected": false,
"fixed": {
"completed": 4815,
"findings": 0,
"questions": 5000,
"seconds": 470.5
},
"unanswered": false,
"vulnerable": {
"completed": 4815,
"findings": 0,
"questions": 5000,
"seconds": 414.2
}
},
"wp-ultimate-member-sqli": {
"detected": false,
"fixed": {
"completed": 76,
"findings": 0,
"questions": 303,
"seconds": 218.0
},
"unanswered": false,
"vulnerable": {
"completed": 76,
"findings": 0,
"questions": 303,
"seconds": 215.8
}
},
"yeswiki-bazar-form-sqli": {
"detected": false,
"fixed": {
"completed": 0,
"findings": 4,
"questions": 1128,
"seconds": 306.9
},
"unanswered": true,
"vulnerable": {
"completed": 0,
"findings": 4,
"questions": 1128,
"seconds": 334.9
}
}
}
},
"fixtures": {
"php-benign": {
"expected": 0,
"false_positives": [
"php-file-inclusion-input:app.php:64",
"php-sql-unquoted-concat:app.php:87"
],
"findings": 2,
"matched": 0,
"missed": [],
"true_positives": []
},
"php-vulnerable": {
"expected": 14,
"false_positives": [],
"findings": 13,
"matched": 13,
"missed": [
"CWE-89"
],
"true_positives": [
"php-code-eval:app.php:46",
"php-command-injection:app.php:41",
"php-file-inclusion-input:app.php:62",
"php-open-redirect:app.php:78",
"php-path-input:app.php:67",
"php-sql-call-composition:app.php:16",
"php-sql-sprintf-unescaped:app.php:32",
"php-sql-unquoted-concat:app.php:27",
"php-sql-unquoted-interpolation:app.php:22",
"php-ssrf-input:app.php:72",
"php-unserialize:app.php:52",
"php-wpdb-sql-composition:app.php:11",
"php-xss-echo-input:app.php:57"
]
}
},
"provenance": {
"analyzer_base_commit": "45d02ec",
"corpus": "development only: benchmarks/push/inputs.json pmpro vulnerable/fixed/benign (role development) and the spent population-v1 PHP cases' vulnerable/fixed pins; population-v2 was not read",
"date": "2026-09-30",
"harness": "measure_php_quick.py (this directory)",
"in_sample": "the rules were written and tightened while looking at these pins and the fixtures, so every number here is in-sample (development), not a holdout result",
"instrument": "every pin reports php_files and php_bytes read by the scan; a zero-byte pin fails the harness",
"known_site": "pmpro: getMemberOrderByCode on each pin; v1: fix hunks (vulnerable side / fixed side, test paths dropped) widened by 10 lines, plus the declared LearnPress sink line"
},
"rules": {
"php-code-eval": {
"fixed": 2,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 3,
"precision_lower_bound": 0.333,
"status": "shadow",
"vulnerable": 2,
"vulnerable_at_site": 0
},
"php-command-injection": {
"fixture_fp": 0,
"fixture_tp": 1,
"n": 1,
"precision_lower_bound": 1.0,
"status": "enabled",
"vulnerable": 0,
"vulnerable_at_site": 0
},
"php-file-inclusion-input": {
"fixture_fp": 1,
"fixture_tp": 1,
"n": 2,
"precision_lower_bound": 0.5,
"status": "enabled",
"vulnerable": 0,
"vulnerable_at_site": 0
},
"php-open-redirect": {
"fixture_fp": 0,
"fixture_tp": 1,
"n": 1,
"precision_lower_bound": 1.0,
"status": "enabled",
"vulnerable": 0,
"vulnerable_at_site": 0
},
"php-path-input": {
"fixture_fp": 0,
"fixture_tp": 1,
"n": 1,
"precision_lower_bound": 1.0,
"status": "enabled",
"vulnerable": 0,
"vulnerable_at_site": 0
},
"php-sql-call-composition": {
"fixed": 3,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 4,
"precision_lower_bound": 0.25,
"status": "shadow",
"vulnerable": 3,
"vulnerable_at_site": 0
},
"php-sql-sprintf-unescaped": {
"fixed": 44,
"fixed_at_site": 3,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 46,
"precision_lower_bound": 0.109,
"status": "enabled",
"vulnerable": 45,
"vulnerable_at_site": 4
},
"php-sql-unquoted-concat": {
"benign": 9,
"fixed": 52,
"fixture_fp": 1,
"fixture_tp": 1,
"n": 58,
"precision_lower_bound": 0.034,
"status": "enabled",
"vulnerable": 56,
"vulnerable_at_site": 1
},
"php-sql-unquoted-interpolation": {
"benign": 28,
"fixed": 62,
"fixed_at_site": 8,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 70,
"precision_lower_bound": 0.129,
"status": "enabled",
"vulnerable": 69,
"vulnerable_at_site": 8
},
"php-ssrf-input": {
"fixture_fp": 0,
"fixture_tp": 1,
"n": 1,
"precision_lower_bound": 1.0,
"status": "enabled",
"vulnerable": 0,
"vulnerable_at_site": 0
},
"php-unserialize": {
"fixed": 3,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 4,
"precision_lower_bound": 0.25,
"status": "shadow",
"vulnerable": 3,
"vulnerable_at_site": 0
},
"php-wpdb-sql-composition": {
"benign": 55,
"benign_at_site": 1,
"fixed": 46,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 63,
"precision_lower_bound": 0.032,
"status": "enabled",
"vulnerable": 62,
"vulnerable_at_site": 1
},
"php-xss-echo-input": {
"fixed": 2,
"fixture_fp": 0,
"fixture_tp": 1,
"n": 3,
"precision_lower_bound": 0.333,
"status": "shadow",
"vulnerable": 2,
"vulnerable_at_site": 0
}
},
"window": 10
}