-
Notifications
You must be signed in to change notification settings - Fork 0
Reduce false positives after CR Self Service plugin scan #118
Description
WPCC Pattern Library — False Positive Review
Source: AI review of creditconnection2-self-service scan
Date: 2026-03-23
Scan findings: 99 total (original) → 31 after all fixes | Estimated true positives: ~25–30
Action Items
✅ Fix Now — High Confidence, Low Effort
-
FIX
php-shell-exec-functions.json—exec-callpattern matchescurl_exec()✅ Fixed in commit 740ba08
Pattern:exec[[:space:]]*\(has no word boundary → matchescurl_exec(.
Fix: Change to\bexec[[:space:]]*\(in theexec-callsub-pattern.
File:dist/patterns/php-shell-exec-functions.json
FPs eliminated: 8 (all CRITICAL — all werecurl_exec($curl)calls) -
php-dynamic-include.json— WP-CLI bootstrap scripts no longer flagged as LFI ✅ Resolved in follow-up commit
Finding:check-user-meta.php:13andtest-alternate-registry-id.php:24—$pathis iterated from a hardcoded static array, never user-controlled.
Attempted fix (740ba08 — insufficient): Addedwp-loadtoexclude_patterns, but the actual matched line isrequire_once $path;— it does not containwp-load.
Proper fix: Added newexclude_if_file_containscapability to the simple pattern runner anddist/bin/check-performance.sh. When a matched file's content contains any string listed in the newexclude_if_file_containsJSON array, all matches in that file are suppressed. Added"wp eval-file"tophp-dynamic-include.jsonunder this key — both WP-CLI scripts have this string in their docblock comment.
Files changed:dist/bin/check-performance.sh(runner feature),dist/patterns/php-dynamic-include.json(new exclusion key)
FPs eliminated: 2 (both CRITICAL)
✅ Implemented After Investigation
-
FIX
spo-002-superglobalsinline grep corruption ✅ Implemented in scanner
Scan log findings: 31 total spo-002 findings. 16 areCURLOPT_POST/CURLOPT_POSTFIELDS, 2 are JStype: 'POST'strings, 4 are$_SERVERreads (SERVER not in the pattern alternation), 1 is the only legitimate finding (line 1014).
Root cause confirmed: The inline bash spo-002 grep (check-performance.sh ~line 3723) uses a double-quoted string with\\$_. In bash double-quotes,\\→\and then$_starts expansion of the bash$_special variable (last argument of the previous command). At runtime,$_contains the last argument fromtext_echo "▸ Direct superglobal manipulation..."— an ANSI-coloured string including[HIGH]. This corrupts the entire ERE pattern, causing it to match incorrectly in a non-deterministic way.
The JSON pattern itself (\$_(GET|POST...)) is correct — tested viaload_pattern+ direct grep, it does NOT match CURLOPT_POST. The bug is entirely in the inline bash code, not the JSON pattern file.
Fix implemented: Changed the inline grep at line 3723 from double-quoted to single-quoted string, which prevents$_expansion. This is a scanner bug, not a pattern bug. The JSON file did not need to change.
File to fix:dist/bin/check-performance.sh~line 3723
Verified impact:spo-002-superglobalsdropped from 31 → 3 findings in the follow-up scan. Remaining 3 are legitimate review cases:$_POST['force_refresh'],unset($_GET['activate']), and$_GET['view_errors']conditional logic. -
FIX simple runner ignoring
exclude_patterns/exclude_files✅ Implemented in scanner
Scan log findings: 30unsanitized-superglobal-readfindings. Confirmed FPs include:class-cr-rest-api.php:90,class-cr-rest-api.php:98,class-cr-rest-api.php:843,class-cr-business-rest-api.php:103,class-cr-business-rest-api.php:138,class-cr-business-rest-api.php:857— all are same-line ternary patterns likeisset($_GET['x']) ? sanitize_text_field($_GET['x']) : ''.
Root cause confirmed: The simple pattern runner (check-performance.sh~line 5970) runscached_grep -E "$pattern_search"but never appliesexclude_patternsfrom the JSON definition. Theexclude_patternsarray inunsanitized-superglobal-read.json(which includessanitize_,isset\(,esc_, etc.) is loaded but silently ignored. The legacy inline checks manually pipe throughgrep -vto apply exclusions; the JSON-driven simple runner does not.
This is NOT a multiline issue — the flagged lines all have the sanitizer wrapper on the same line. The exclusion simply isn't being applied at all by the simple runner.
Additional FPs from same root cause:clear-person-cache.php:34,setup-user-registry-id.php:23-24,set-account-type.php:26-27— all properly guarded$_POSTreads with nonce verification on the same line.
Fix implemented: The simple pattern runner now parses bothexclude_patternsandexclude_filesfrom the JSON pattern file and filters matches before JSON findings are added. This improves behavior across all JSON-definedgrep/simplepatterns, not justunsanitized-superglobal-read.
File to fix:dist/bin/check-performance.sh~line 5970 (simple pattern runner grep call)
Verified impact:unsanitized-superglobal-readdropped from 30 → 19 findings in the follow-up scan. The remaining 19 are mostly other classes of reads that still require separate tuning, especially the dedicatedunsanitized-superglobal-isset-bypassrule. -
FIX admin-only hook whitelist for capability check false positives ✅ Implemented in scanner
Finding:credit-registry-forms.php:48—add_action('admin_notices', ...)flagged for missing capability check.admin_noticesonly fires for authenticated admin users.
Reviewer recommendation: Whitelist inherently-admin-only hooks (admin_notices,admin_init,admin_menu, etc.)
Fix implemented: Added admin-only hook whitelist in thespo-004-missing-cap-checksection ofcheck-performance.sh(~line 4261). Whenadd_action()uses a whitelisted hook, the finding is still recorded but downgraded to INFO severity instead of HIGH. Whitelisted hooks:admin_notices,admin_init,admin_menu,admin_head,admin_footer,admin_enqueue_scripts,admin_print_styles,admin_print_scripts,network_admin_menu,user_admin_menu,network_admin_notices,admin_bar_init,admin_action_*(glob),load-*(glob).
File changed:dist/bin/check-performance.sh~line 4261
FPs eliminated: 1+ per scan (downgraded to INFO)
✅ Previously Deferred — Now Implemented
- FIX N+1 loop detection now verifies lexical containment ✅ Implemented in scanner
Finding 1:check-user-meta.php:23—get_user_meta()called sequentially for a single user, not inside a user loop.
Finding 2:class-cr-business-rest-api.php:245— singleget_user_meta()re-read after processing.
Root cause:find_meta_in_loop_line()used a simple line-range check (loop line + 80 lines forward) without verifying the meta call was lexically inside the loop's braces. Sequentialget_*_meta()calls after a loop's closing}were incorrectly flagged.
Fix implemented: Replaced the line-rangeawkinfind_meta_in_loop_line()with brace-depth tracking. The newawkcounts{and}from the loop line forward, only matchingget_(post|term|user)_metawhile depth > 0. Once the loop body closes (depth returns to 0), scanning stops — meta calls after the loop are no longer flagged.
File changed:dist/bin/check-performance.sh~line 5413 (find_meta_in_loop_line())
FPs eliminated: 2+ per scan
📋 Round 2 — Post-Scan Analysis (2026-03-24)
-
FIX
limit-multiplier-from-count— nearly 100% FPs, no multiplier context ✅ Implemented in pattern
Findings: 24 findings, all arecount()used for display (echo), array key assignment, or loop comparison (count($x) < $length). Zero arecount()multiplied into a SQLLIMITclause.
Root cause: The JSONsearch_patternwascount\((matching anycount()call). The inline check at ~line 5122 correctly requirescount(...) * <number>, but the simple runner ran the broader JSON pattern separately.
Fix: Tightened JSONsearch_patterntocount\([^)]*\)[[:space:]]*\*[[:space:]]*[0-9]— now requires the multiplier operator, matching only the inline check's intent.
File changed:dist/patterns/limit-multiplier-from-count.json
Verified impact: 24 → 0 findings (all were FPs) -
FIX
rest-no-pagination— flags non-GET action endpoints ✅ Implemented in scanner + pattern
Findings: 16 findings. Routes like/business/refresh,/person/switch-user,/business/submit-updateuse POST/PUT/DELETE — pagination is inapplicable.
Root cause: The validator checked 15-line context for pagination keywords but didn't account for HTTP method.
Fix: Addedskip_if_context_matchescapability to the scripted pattern runner. When a match's narrow context (3 lines) contains a pattern like'methods' => 'POST', the finding is suppressed before the validator runs. Added the method-detection pattern torest-no-pagination.json.
Files changed:dist/bin/check-performance.sh(scripted runner),dist/patterns/rest-no-pagination.json(newskip_if_context_matcheskey)
Verified impact: 16 → 8 findings (8 POST/PUT/DELETE endpoints suppressed; 8 GET endpoints correctly retained) -
FIX cross-rule deduplication for overlapping superglobal findings ✅ Implemented in scanner
Findings: 14 uniquefile:linelocations appeared in 2–4 rules simultaneously (spo-002-superglobals,unsanitized-superglobal-read,unsanitized-superglobal-isset-bypass).
Root cause: Three superglobal rules overlap in scope but ran independently with no dedup.
Fix: Added a deduplication pass in the JSON report builder (~line 1683). For a defined set of overlapping rule IDs, when the samefile:lineappears in multiple rules, only the first (highest-priority) finding is kept. Uses a seen-keys set for O(n) dedup.
File changed:dist/bin/check-performance.sh(JSON report builder)
Verified impact: Eliminated all cross-rule duplicates — 0 remaining duplicate file:line locations in scan output. Total superglobal findings: 36 → 13 (spo-002: 3, unsanitized-read: 10, isset-bypass: 0 after dedup)
✔️ No Action Required — Already Handled or Misdiagnosed
-
SKIP —
isset()exclusion for superglobal reads
isset\(is already inexclude_patternsforunsanitized-superglobal-read.json. Reviewer's suggestion is already implemented. -
SKIP —
$_prefix anchoring for superglobal manipulation
All three sub-patterns inspo-002-superglobalsalready require$_prefix. The reviewer's suggested fix is already in place. -
SKIP — Sanitizer negative-lookbehind regex for
unsanitized-superglobal-read
Theexclude_patternslist already handles same-line sanitizer wrapping. The multiline case is a structural grep limitation, not addressable by the proposed regex.
Valid Issues Found (Not FPs — Tracker for Plugin Owner)
| # | File | Line | Issue | Risk |
|---|---|---|---|---|
| 6 | admin-test-page.php |
191 | $_GET['view_file'] used without sanitize_file_name(); strpos($view_file, '..') bypass-able via encoding |
HIGH |
| 7 | admin-test-page.php |
145 | $_GET['view_dir'] displayed with esc_html() before sanitize_file_name() on line 147 — safe but misordered |
LOW (confusing) |
| 8 | api-functions.php |
1014 | $_POST['force_refresh'] in AJAX handler — strict === 'true' comparison limits injection, but verify nonce upstream |
LOW–MEDIUM |
Impact Summary
| Fix | File to Edit | Effort | FPs Eliminated | Status |
|---|---|---|---|---|
\b word boundary on exec-call |
php-shell-exec-functions.json |
1 line | 8 | ✅ Done (740ba08) |
exclude_if_file_contains + wp eval-file |
check-performance.sh + php-dynamic-include.json |
Medium | 2 verified | ✅ Done |
| Single-quote inline spo-002 grep | check-performance.sh ~L3723 |
1 line | 28 verified | ✅ Done |
Apply exclude_patterns in simple runner |
check-performance.sh ~L5970 |
Medium | 11 verified | ✅ Done |
| Admin-only hook whitelist | check-performance.sh ~L4261 |
Low | 1+ per scan (→ INFO) | ✅ Done |
| N+1 loop containment (brace-depth) | check-performance.sh ~L5413 |
Medium | 2+ per scan | ✅ Done |
Tighten limit-multiplier-from-count pattern |
limit-multiplier-from-count.json |
1 line | 24 verified | ✅ Done |
skip_if_context_matches for rest-no-pagination |
check-performance.sh + rest-no-pagination.json |
Medium | 8 verified | ✅ Done |
| Cross-rule dedup for superglobal findings | check-performance.sh (JSON builder) |
Medium | 23 duplicates | ✅ Done |
Latest measured totals: 99 → 88 → 86 → 31 findings (2026-03-24, after Round 2 fixes).