The best black-box WordPress security scanner
Project description
Plecost
Professional WordPress Security Scanner
Async-first, library-friendly, no external API required.
Table of Contents
- What is Plecost?
- Plecost vs WPScan
- Quick Start
- Installation
- CVE Database
- CVE Detection Engine
- Scanning
- Detection Modules
- WooCommerce Security
- WP eCommerce Security
- Output Formats
- Library Usage
- Environment Variables
- Architecture
- Troubleshooting
- Local Test Environment
- License
What is Plecost?
Plecost detects vulnerabilities in WordPress installations — core, plugins, and themes — and correlates findings against a daily-updated local CVE database. It runs as a CLI tool, a Python library, or inside task queues like Celery, with a consistent and automation-friendly output format.
No Ruby. No API key. No subscription. No data sent to third parties on every scan.
Plecost vs WPScan
Plecost was built from scratch to fix the limitations teams hit in production when using WPScan: API rate caps, external data dependencies, no library API, no async architecture, and a narrow detection surface.
At a Glance
| Capability | Plecost v4 | WPScan |
|---|---|---|
| Language / runtime | Python 3.11+ | Ruby |
| Async concurrent scanning | ✅ httpx + asyncio | ❌ |
| Python library API | ✅ from plecost import Scanner |
❌ |
| API key required | ❌ never | ⚠️ required for CVE data |
| CVE data — free tier limit | ✅ unlimited (local DB) | ❌ 25 API tokens/day |
| Offline scanning (CVEs included) | ✅ | ❌ |
| Data sent to third parties on scan | ❌ none | ⚠️ every request |
| Docker native | ✅ | ✅ |
| Celery / task queue compatible | ✅ | ❌ |
| PostgreSQL support (shared team) | ✅ | ❌ |
Scanning Capabilities
| Capability | Plecost v4 | WPScan |
|---|---|---|
| Fast mode (top 150 plugins / 50 themes) | ✅ | ❌ |
| Deep mode (4,750+ plugins / 900+ themes) | ✅ | ✅ |
| Configurable concurrency (10–50 requests) | ✅ | ✅ |
| Stealth mode (random UA + passive only) | ✅ | ✅ |
| Aggressive mode (50 parallel requests) | ✅ | ✅ |
| Authenticated scans | ✅ | ✅ |
| Proxy support (HTTP + SOCKS5) | ✅ | ✅ |
| Bulk scan (multiple URLs from file) | ✅ | ❌ |
| Pre-flight 403 detection (aborts cleanly) | ✅ | ❌ |
Per-module options (--module-option) |
✅ | ❌ |
| Run / skip individual modules | ✅ | ✅ |
Selective module list (--modules) |
✅ | ❌ |
| Auto-retry with SSL verification disabled | ✅ | ❌ |
Vulnerability Detection
| Capability | Plecost v4 | WPScan |
|---|---|---|
| WordPress core fingerprinting | ✅ (meta, readme, RSS, wp-login) | ✅ |
| Plugin detection — passive HTML | ✅ | ✅ |
| Plugin detection — active wordlist | ✅ 4,750+ slugs | ✅ |
| Theme detection — passive HTML | ✅ | ✅ |
| Theme detection — active wordlist | ✅ 900+ themes | ✅ |
| CVE correlation (core + plugins + themes) | ✅ local DB, daily NVD sync, <5% FP rate | ✅ API |
| Exploit availability flag per CVE | ✅ | ✅ |
| CVSS scores per finding | ✅ | ✅ |
| WAF / CDN detection | ✅ 7 providers | ⚠️ limited |
| User enumeration — REST API | ✅ | ✅ |
| User enumeration — author archives | ✅ | ✅ |
| XML-RPC — access, pingback DoS, method list | ✅ 3 checks | ⚠️ basic |
| REST API — disclosure, oEmbed, CORS | ✅ 3 checks | ❌ |
| HTTP security headers | ✅ 8 checks (HSTS, CSP, X-Frame…) | ❌ |
| SSL / TLS misconfiguration | ✅ 3 checks | ❌ |
| Misconfiguration (wp-config, .env, .git…) | ✅ 12 checks | ⚠️ partial |
| Directory listing (wp-content subdirs) | ✅ | ❌ |
| Debug mode / PHP version disclosure | ✅ | ❌ |
| Open user registration | ✅ | ❌ |
| Content / card skimmer analysis | ✅ scripts, iframes, hardcoded keys | ❌ |
| Webshell detection | ✅ 147–523 paths | ❌ |
| Malicious upload detection (PHP in uploads) | ✅ | ❌ |
| WooCommerce dedicated module | ✅ 22 checks | ❌ |
| WP eCommerce dedicated module | ✅ 22 checks | ❌ |
| Semi-active eCommerce CVE probes | ✅ boolean-only, no time-based | ❌ |
| WooCommerce REST API auth bypass | ✅ CVE-2023-28121 | ❌ |
| WooCommerce IDOR / PII disclosure | ✅ CVE-2023-34000 | ❌ |
Output and Integration
| Capability | Plecost v4 | WPScan |
|---|---|---|
| Rich terminal output (color-coded) | ✅ | ✅ |
| JSON output (stable schema) | ✅ | ✅ |
Verbose real-time progress (-v) |
✅ | ✅ |
| Quiet mode (HIGH + CRITICAL only) | ✅ | ❌ |
| Stable permanent finding IDs | ✅ 120 IDs (PC-MOD-NNN) |
❌ |
plecost explain <ID> — per-finding remediation |
✅ | ❌ |
| Safe to track findings in JIRA / ticketing | ✅ IDs never change | ❌ |
Remediation ID per finding (REM-MOD-NNN) |
✅ | ❌ |
| i18n / multilingual output | ✅ EN + ES | ❌ |
| Included vulnerable test environment (DVWP) | ✅ Docker Compose | ❌ |
Data Independence
WPScan sends every scan to
wpscan.comto look up CVE data. On the free tier you get 25 API tokens per day — enough for a handful of targets. Plecost keeps the entire CVE database locally, updated daily via GitHub Actions with no per-scan network call to any third-party API.
| Plecost v4 | WPScan | |
|---|---|---|
| Local CVE database (SQLite / PostgreSQL) | ✅ | ❌ |
| Data sent externally on each scan | ❌ none | ✅ to wpscan.com |
| CVE updates mechanism | GitHub Actions, NVD API v2 | SaaS subscription |
| Daily incremental patch download | ✅ (< 100 KB/day) | — |
| First-run full database download | ✅ plecost update-db |
— |
| Works fully air-gapped after DB download | ✅ | ❌ |
| SHA256 integrity check before download | ✅ | ❌ |
Quick Start
pip install plecost
# Download the CVE database (first time only — takes a few seconds)
plecost update-db
# Scan a target
plecost scan https://target.wordpress.com
That's it. No account, no API key, no daemon running in the background.
Installation
pip
pip install plecost
pip install plecost[fast] # adds uvloop for higher throughput
pip install plecost[postgres] # adds asyncpg for PostgreSQL support
Docker
docker run --rm ghcr.io/plecost/plecost scan https://target.com
# Save JSON report to local directory
docker run --rm -v $(pwd):/data ghcr.io/plecost/plecost scan https://target.com \
--output /data/report.json
From source
git clone https://github.com/Plecost/plecost.git
cd plecost
pip install -e ".[dev]"
CVE Database
Plecost ships with a local SQLite database covering WordPress core, plugins, and themes. It lives at ~/.plecost/db/plecost.db and is never sent to any external service during scans.
The database needs to be downloaded once before the first scan, and kept up to date thereafter.
First-time setup
plecost update-db
This downloads a pre-built snapshot from plecost-db releases (~10–50 MB). Subsequent runs only download the daily diff — typically under 100 KB.
Keeping it current
Run update-db regularly (weekly is fine for most use cases):
plecost update-db
Plecost checks a SHA256 checksum before downloading anything. If nothing changed since your last run, no data is transferred.
How the update mechanism works
The plecost-db repository runs a GitHub Actions workflow daily at 02:00 UTC. It queries the NVD API v2.0 for all WordPress-related CVEs modified in the last 24 hours, applies Jaro-Winkler fuzzy matching to correlate CVE product names against ~50,000 known plugin/theme slugs, and publishes a small JSON patch file as a release asset.
When you run plecost update-db, it:
- Downloads
index.jsonfrom theplecost-dbreleases (64 bytes) - Compares its SHA256 against the local copy
- Downloads only the missing patch files and applies them in order
- On first run, downloads
full.jsoninstead (complete history)
| Run | What's downloaded | Typical size |
|---|---|---|
| First time | full.json (all CVEs) |
10–50 MB |
| Daily update | today's patch | < 100 KB |
| Already up to date | nothing (checksum match) | 64 bytes |
Custom database location
# SQLite at a custom path
export PLECOST_DB_URL=sqlite:////data/plecost.db
plecost update-db
plecost scan https://target.com
# PostgreSQL (shared team setup)
pip install plecost[postgres]
export PLECOST_DB_URL=postgresql+asyncpg://user:pass@host/plecost
plecost update-db
plecost scan https://target.com
What's New in v4.8.0
CVE evidence now includes installed version
CVE findings include the detected installed version in their evidence object. This makes it clear exactly which version triggered each CVE — no ambiguity about why the finding was reported.
{
"id": "PC-CVE-CVE-2024-2262",
"evidence": {
"cve_id": "CVE-2024-2262",
"software": "woocommerce-products-filter",
"version_range": "*–1.4.4",
"installed_version": "1.3.8.2"
}
}
XML bomb protection in sitemap/RSS parsing
The users module now caps XML response size at 1 MB before parsing with ElementTree. Malformed or adversarially large XML responses (XML bomb DoS) are silently skipped instead of potentially exhausting memory.
What's New in v4.5.0
Brute force: explicit wordlist activation
--brute-force alone now does not try any passwords. You must explicitly request a password source:
| Before (v4.4) | After (v4.5) |
|---|---|
--brute-force → uses internal ~70 passwords silently |
--brute-force → no passwords; warns with PC-BRU-008 |
(no --brute-force-defaults flag) |
--brute-force --brute-force-defaults → internal list |
This prevents accidental brute-force attempts when --brute-force is added to a scan without a deliberate wordlist choice.
run_many() callback propagation
Scanner.run_many() now propagates on_finding, on_module_start, on_module_done, and on_module_progress callbacks to each per-target Scanner. Previously these callbacks were silently dropped during bulk scans.
23 new finding IDs
v4.5.0 adds 23 new permanent finding IDs across new modules:
| Module | New finding IDs | Description |
|---|---|---|
misconfigs |
PC-MCFG-013 – PC-MCFG-020 | Extended misconfiguration checks |
debug_exposure |
PC-DBG-002 | Extended debug checks |
xmlrpc |
PC-XMLRPC-007 | Additional XML-RPC check |
timthumb |
PC-TTH-001, PC-TTH-002, PC-TTH-003 | TimThumb vulnerability detection |
emergency_scripts |
PC-EMRG-001 – PC-EMRG-005 | Emergency script detection |
brute (XML-RPC) |
PC-BRUTE-001 – PC-BRUTE-004 | XML-RPC brute force findings |
wp_hardening |
PC-WPH-001 | WordPress hardening checks |
brute_force |
PC-BRU-008 | No wordlist configured warning |
All IDs are permanent and supported by plecost explain <ID>.
CVE Detection Engine
v4.4: CVE engine reliability — 15 database bugs fixed, per-window sync checkpointing, Rich markup escaping.
v4.3: New multi-stage normalization pipeline — from 70% false positive rate to under 5%.
Plecost's CVE detection has always used a local database built from NVD data. What changed in v4.3 is how NVD vulnerability records are mapped to WordPress plugin slugs — the step where previous tools (including earlier Plecost versions) silently generate false positives.
What's new in v4.4
v4.4 is a reliability release focused on the CVE database engine:
- Rejected CVEs now rehabilitable: re-upserting a previously-deleted CVE removes it from the suppression list automatically (G2-003)
- Upsert-wins-delete in same patch: a CVE listed in both
upsert[]anddelete[]of the same daily patch is kept, not suppressed (G2-002) - Date poisoning fixed: malformed dates in patch files no longer corrupt
last_patch_date, which previously caused all subsequent patches to be silently ignored (G-002) - Batch deduplication: duplicate
(cve_id, slug)pairs within a single patch no longer causeIntegrityErrorrollbacks (G-012) - theme.vulns populated: the CVE module now surfaces known vulnerabilities for detected themes, matching what it already did for plugins (G2-005)
- Single
rejected_idsquery per scan: previously,SELECT rejected_cveswas executed once per plugin (300+ queries in fast-scan mode); now fetched once and reused (G-016) run_many()respects custom DB: bulk scans (-T urls.txt) now correctly use a custom--dbpath instead of falling back to the default location (G2-001)- Incremental sync checkpointing:
sync-dbnow saves progress after each completed 90-day NVD window; a transient HTTP 429 no longer forces a full re-sync from scratch (AC-13) - NVD 429 retry with backoff: rate-limit responses from the NVD API trigger automatic retry with exponential backoff instead of silent data loss (AC-14)
- 44 new tests:
_is_affected()boundary coverage,find_all_by_slug(),rejected_cvesfiltering, end-to-end CVE pipeline integration tests; total coverage 84.4%
The Problem with Fuzzy Matching
NVD names plugins using CPE syntax (element_pack, word_replacer_pro). WordPress.org identifies them by slug (bdthemes-element-pack-lite, word-replacer-ultra). These are rarely the same string.
Previous approaches used fuzzy string matching (Jaro-Winkler, Levenshtein) to bridge this gap. The problem: phonetic similarity is not semantic correctness. element_pack scores 0.88 against elementor — higher than the actual match bdthemes-element-pack-lite (0.73). The result: Wordfence installed → CVEs for a completely different plugin reported. False positive.
The v4.3 Solution: Cascaded Deterministic Signals
The new engine uses a priority cascade, stopping at the first high-confidence signal:
1. Canonical exact match → contact_form_7 → contact-form-7 [conf=1.0]
2. URL reference extraction → CVE refs often → explicit slug in URL [conf=0.95]
(65-70% of WordPress CVEs in NVD contain direct wordpress.org/plugins/{slug} links)
3. Jaro-Winkler ≥ 0.90 → stricter threshold eliminates known FPs [conf=score]
4. No match → CVE not inserted (silence > false alarm)
Signal #2 is the key insight: 65-70% of WordPress CVEs in NVD already contain the correct slug in their reference URLs (plugins.trac.wordpress.org/.../bdthemes-element-pack-lite/). Extracting it directly gives certainty — not approximation.
Results
Measured against a live WooCommerce site with 40+ installed plugins:
| Before (v4.2) | After (v4.3) | |
|---|---|---|
| Total findings | 41 | 12 |
| False positives | 29 (71%) | 0 |
| Accuracy | 29% | 100% |
The 12 remaining findings were all independently verified as true positives via manual HTTP requests.
Scanning
Basic scan
$ plecost scan https://target.com
Plecost v4.4 — WordPress Security Scanner
Target: https://target.com
WordPress 6.4.2 detected | WAF: Cloudflare
Plugins (3)
woocommerce 8.2.1 VULNERABLE
contact-form-7 5.8 OK
elementor 3.17.0 OK
Findings (7)
PC-CVE-CVE-2023-28121 WooCommerce SQLi CRITICAL
PC-SSL-001 HTTP does not redirect to HTTPS HIGH
PC-HDR-001 Missing Strict-Transport-Security MEDIUM
PC-USR-001 User enumeration via REST API MEDIUM
PC-XMLRPC-001 XML-RPC interface accessible MEDIUM
PC-REST-001 REST API user data exposed LOW
PC-MCFG-009 readme.html discloses WP version LOW
Summary: 1 Critical 1 High 3 Medium 2 Low | Duration: 4.2s
Common options
# Authenticated scan
plecost scan https://target.com --user admin --password secret
# Route traffic through Burp Suite or OWASP ZAP
plecost scan https://target.com --proxy http://127.0.0.1:8080
# Run only specific detection modules
plecost scan https://target.com --modules fingerprint,plugins,cves
# Aggressive mode — 50 parallel requests (use on internal targets)
plecost scan https://target.com --aggressive
# Deep mode — full wordlist (4750+ plugins, 900+ themes); default scans top 150/50
plecost scan https://target.com --deep
# Stealth mode — random UA, passive detection only, slower
plecost scan https://target.com --stealth
# Save results as JSON
plecost scan https://target.com --output report.json
# Show only HIGH and CRITICAL findings
plecost scan https://target.com --quiet
All scan flags
| Flag | Description | Default |
|---|---|---|
--concurrency N |
Parallel requests | 10 |
--timeout N |
Request timeout (seconds) | 10 |
--proxy URL |
HTTP or SOCKS5 proxy | — |
--user / -u |
WordPress username | — |
--password / -p |
WordPress password | — |
--modules |
Modules to run (comma-separated) | all |
--skip-modules |
Modules to skip | — |
--stealth |
Passive mode, random UA, slower pacing | off |
--aggressive |
Max concurrency (50 requests) | off |
--output / -o |
JSON output file | — |
--no-verify-ssl |
Skip certificate verification | off |
--force |
Scan even if WordPress not detected | off |
--deep |
Full wordlist scan (4750+ plugins, 900+ themes); default is top 150/50 | off |
--verbose / -v |
Real-time module progress and findings during scan | off |
--quiet |
Show only HIGH and CRITICAL findings | off |
--module-option |
Module-specific option: MODULE:KEY=VALUE (repeatable) |
— |
--brute-force |
Enable brute force module (requires --brute-force-defaults or --brute-force-wordlist) |
off |
--brute-force-defaults |
Use built-in password list (~70 passwords + username variants) | off |
--brute-force-wordlist <path> |
External password file (one per line) | — |
--brute-force-combine |
Merge external wordlist with internal list (requires --brute-force-defaults) |
off |
Detection Modules
Plecost runs 19 async modules in parallel, wired through an explicit dependency graph. Modules without interdependencies run concurrently from the start; cves waits for plugins and themes to complete before correlating results against the local database.
| Module | What it checks | Finding IDs |
|---|---|---|
fingerprint |
WordPress version (meta, readme, RSS, wp-login) | PC-FP-001/002 |
waf |
WAF/CDN detection (Cloudflare, Sucuri, Wordfence, Imperva, AWS, Akamai, Fastly) | PC-WAF-001 |
plugins |
Plugin enumeration — passive HTML + brute-force against readme.txt |
PC-PLG-NNN |
themes |
Theme detection via passive scan + style.css brute-force |
PC-THM-001 |
users |
User enumeration via 7 techniques: REST API (paginated), author archives (1–25/100 IDs), WP sitemap, RSS <dc:creator>, Yoast sitemap, oEmbed, HTML author links |
PC-USR-001/002/003/004/005 |
brute_force |
Credential brute force against wp-login.php using detected usernames + internal wordlist or external file. Requires --brute-force. Detects rate-limiting and 2FA. |
PC-BRU-001/002/003/004/007 |
xmlrpc |
XML-RPC access, pingback.ping DoS vector, system.listMethods |
PC-XMLRPC-001/002/003 |
rest_api |
REST API link disclosure, oEmbed, CORS misconfiguration | PC-REST-001/002/003 |
misconfigs |
12 checks: wp-config.php, .env, .git, debug.log, directory traversal... |
PC-MCFG-001–012 |
directory_listing |
Open directory listing in wp-content/ subdirs |
PC-DIR-001–004 |
http_headers |
Missing HSTS, CSP, X-Frame-Options, X-Content-Type, Referrer-Policy... | PC-HDR-001–008 |
ssl_tls |
HTTP→HTTPS redirect, certificate validity, HSTS preload | PC-SSL-001/002/003 |
debug_exposure |
Active WP_DEBUG, PHP version disclosure via response headers |
PC-DBG-001/003 |
content_analysis |
Card skimming scripts, suspicious iframes, hardcoded API keys | PC-CNT-001/002/003 |
auth |
Authenticated checks: login verification, open user registration | PC-AUTH-001/002 |
cves |
CVE correlation for core + plugins + themes against local DB | PC-CVE-{CVE-ID} |
webshells |
Webshell detection across upload paths (147–523 paths depending on mode) | PC-WS-NNN |
woocommerce |
WooCommerce-specific security checks (see below) | PC-WC-000–021 |
wp_ecommerce |
WP eCommerce-specific security checks (see below) | PC-WPEC-000–021 |
Use plecost explain <ID> for full technical detail and remediation steps on any finding ID.
User Enumeration
The users module runs 7 techniques in parallel and deduplicates results across all sources:
| Finding ID | Technique | Severity | Trigger |
|---|---|---|---|
PC-USR-001 |
REST API /wp-json/wp/v2/users with full pagination |
MEDIUM | Endpoint returns user data |
PC-USR-002 |
Author archive /?author=N (IDs 1–25 fast / 1–100 deep, early-stop after 3 misses) |
MEDIUM | Redirect to /author/<slug>/ |
PC-USR-003 |
WordPress core sitemap /wp-sitemap-users-1.xml |
INFO | File accessible and contains <loc> entries with author slugs |
PC-USR-004 |
RSS feed <dc:creator> tags at /?feed=rss2 |
MEDIUM | Feed contains author names |
PC-USR-005 |
Yoast SEO author sitemap /author-sitemap.xml |
INFO | Yoast sitemap accessible |
| (no finding) | oEmbed API — only if author_url contains /author/ |
— | Used for deduplication only |
| (no finding) | Homepage HTML author links matching /author/<slug>/ |
— | Used for deduplication only |
All seven techniques run concurrently. Users discovered by multiple techniques are deduplicated by username (case-insensitive) and appear once in ScanResult.users.
Brute Force
The brute_force module is opt-in and requires --brute-force. It never activates with --aggressive.
Flags
| Flag | Description |
|---|---|
--brute-force |
Enable brute force. No passwords are tried by default — you must also supply --brute-force-defaults and/or --brute-force-wordlist. |
--brute-force-defaults |
Activate the built-in password list (~70 common passwords + dynamic username variants). Requires --brute-force. |
--brute-force-wordlist <path> |
Use an external password file (one password per line). Requires --brute-force. |
--brute-force-combine |
Merge external wordlist with the internal list. Only merges internal passwords when --brute-force-defaults is also set. |
# Enable brute force with built-in wordlist only
plecost scan https://target.com --brute-force --brute-force-defaults
# Enable brute force with an external wordlist only
plecost scan https://target.com --brute-force --brute-force-wordlist passwords.txt
# Combine both (external + internal ~70 common passwords)
plecost scan https://target.com --brute-force --brute-force-defaults \
--brute-force-wordlist passwords.txt --brute-force-combine
Library API:
ScanOptions(
url="https://target.com",
brute_force=True,
brute_force_use_defaults=True, # enables built-in ~70 password list
brute_force_wordlist="/path/to/list.txt", # optional external wordlist
brute_force_combine=True, # merge external + internal
)
For each detected username, --brute-force-defaults tests ~70 internal passwords plus dynamic variants (e.g. username, username123, username2025, username!). External wordlists are capped at 10 000 lines (fast mode) or 50 000 lines (deep mode).
Login success is detected by the presence of a wordpress_logged_in_* cookie in the response (Set-Cookie header).
| Finding ID | Condition | Severity |
|---|---|---|
PC-BRU-001 |
Valid credentials found | CRITICAL (9.8) without 2FA; HIGH (7.5) with 2FA detected |
PC-BRU-002 |
2FA detected on wp-admin login page |
INFO |
PC-BRU-003 |
No WordPress users discovered — brute force skipped | INFO |
PC-BRU-004 |
Brute force completed with no valid credentials found | INFO |
PC-BRU-007 |
Brute force stopped — target is rate-limiting login attempts | INFO |
PC-BRU-008 |
Brute force enabled but no wordlist configured (neither --brute-force-defaults nor --brute-force-wordlist provided) |
INFO |
Passwords in PC-BRU-001 evidence are masked: first 2 characters + *** + last character (e.g. pa***d). Rate limiting is detected by HTTP 429/403, body strings (Too many, Locked, Blocked, etc.), or a dropped TCP connection.
WooCommerce Security
The woocommerce module performs dedicated security checks for WooCommerce installations and its official extensions (Payments, Blocks, Stripe Gateway). It runs automatically when WooCommerce is detected.
Passive checks (always on)
- Fingerprinting — detects WooCommerce version, active extensions (Payments, Blocks, Stripe Gateway), and exposed API namespaces
- REST API without authentication — checks whether
/wp-json/wc/v3/customers,/orders,/coupons, and/system-statusare accessible without credentials (CRITICAL/HIGH) - Sensitive file exposure — directory listing on
/wp-content/uploads/wc-logs/, access to/wp-content/uploads/woocommerce_uploads/
Semi-active checks (opt-in)
Semi-active checks send additional HTTP requests that could leave traces in server logs. Enable explicitly:
plecost scan https://target.com --module-option woocommerce:mode=semi-active
| Check | CVE | CVSS |
|---|---|---|
| WooCommerce Payments authentication bypass | CVE-2023-28121 | 9.8 Critical |
| WooCommerce Stripe Gateway IDOR (PII disclosure) | CVE-2023-34000 | 7.5 High |
Authenticated checks (optional)
Provide WooCommerce REST API credentials to unlock additional checks (system configuration disclosure, payment gateway enumeration):
plecost scan https://target.com \
--module-option woocommerce:wc_consumer_key=ck_xxx \
--module-option woocommerce:wc_consumer_secret=cs_xxx
WooCommerce JSON output
When WooCommerce is detected, the scan result includes a dedicated woocommerce section:
{
"woocommerce": {
"detected": true,
"version": "8.5.2",
"active_plugins": ["core", "payments", "blocks", "stripe-gateway"],
"api_namespaces": ["wc/store/v1", "wc/v3"]
}
}
WP eCommerce Security
The wp_ecommerce module performs dedicated security checks for the WP eCommerce (wp-e-commerce) plugin. It runs automatically when WP eCommerce is detected.
Important: WP eCommerce has been abandoned since 2020 (last version: 3.15.1). All current installations are vulnerable to unpatched CVEs. PC-WPEC-003 is always emitted when the plugin is detected.
Passive checks (always on)
- Fingerprinting — detects version via
readme.txt, active payment gateways (ChronoPay) - Directory exposure — plugin directory listing,
uploads/wpsc/,uploads/wpsc/digital/(digital downloads) - Admin scripts — direct access to
wpsc-admin/db-backup.phpandwpsc-admin/display-log.php - ChronoPay endpoint — callback endpoint accessibility check
Semi-active checks (opt-in)
Enable explicitly:
plecost scan https://target.com --module-option wpec:mode=semi-active
| Check | CVE | CVSS |
|---|---|---|
| ChronoPay SQL Injection | CVE-2024-1514 | 9.8 Critical |
| PHP Object Injection via AJAX | CVE-2026-1235 | 8.1 High |
Detection is boolean-only (SQL error strings, deserialization patterns) — no time-based probes.
WP eCommerce JSON output
When WP eCommerce is detected, the scan result includes a dedicated wp_ecommerce section:
{
"wp_ecommerce": {
"detected": true,
"version": "3.15.1",
"active_gateways": ["chronopay"],
"checks_run": ["readme", "directories", "sensitive_files", "chronopay_endpoint"]
}
}
Output Formats
Terminal (default)
Rich-formatted tables with color-coded severities. Use --quiet to suppress LOW/MEDIUM findings.
JSON
plecost scan https://target.com --output report.json
{
"url": "https://target.com",
"scanned_at": "2026-04-13T09:00:00Z",
"is_wordpress": true,
"wordpress_version": "6.4.2",
"waf_detected": "Cloudflare",
"plugins": [{ "slug": "woocommerce", "version": "8.2.1" }],
"themes": [{ "slug": "twentytwentyfour", "version": "1.2" }],
"users": ["admin", "editor"],
"findings": [
{
"id": "PC-CVE-CVE-2023-28121",
"remediation_id": "REM-CVE-CVE-2023-28121",
"title": "WooCommerce SQLi (CVE-2023-28121)",
"severity": "CRITICAL",
"cvss_score": 9.8,
"description": "...",
"remediation": "Update WooCommerce to version 7.8.0 or later.",
"references": ["https://nvd.nist.gov/vuln/detail/CVE-2023-28121"],
"evidence": {
"cve_id": "CVE-2023-28121",
"software": "woocommerce",
"version_range": "*–7.8.0",
"installed_version": "8.2.1"
},
"module": "cves"
}
],
"summary": { "critical": 1, "high": 1, "medium": 3, "low": 2 },
"duration_seconds": 4.2,
"blocked": false,
"woocommerce": {
"detected": true,
"version": "8.2.1",
"active_plugins": ["core", "payments", "blocks"],
"api_namespaces": ["wc/store/v1", "wc/v3"]
},
"wp_ecommerce": null
}
Library Usage
Plecost is a first-class Python library. The same logic that powers the CLI is available as an importable API — no subprocess, no parsing CLI output.
Standalone script
import asyncio
from plecost import Scanner, ScanOptions
async def main():
options = ScanOptions(
url="https://target.com",
concurrency=10,
timeout=10,
modules=["fingerprint", "plugins", "cves"], # None = all modules
)
result = await Scanner(options).run()
print(f"WordPress {result.wordpress_version} | WAF: {result.waf_detected}")
for finding in result.findings:
print(f"[{finding.severity.value}] {finding.id}: {finding.title}")
asyncio.run(main())
Real-time callbacks
Scanner accepts four optional callbacks for real-time progress reporting:
import asyncio
from plecost import Scanner, ScanOptions
def on_finding(finding):
print(f"[{finding.severity.value}] {finding.id}: {finding.title}")
def on_module_start(module_name: str):
print(f" → Starting module: {module_name}")
def on_module_done(module_name: str):
print(f" ✓ Done: {module_name}")
def on_progress(module_name: str, current: int, total: int):
pct = int(current / total * 100) if total else 0
print(f" [{module_name}] {pct}% ({current}/{total})")
async def main():
opts = ScanOptions(url="https://target.com")
result = await Scanner(
opts,
on_finding=on_finding,
on_module_start=on_module_start,
on_module_done=on_module_done,
on_module_progress=on_progress,
).run()
asyncio.run(main())
Bulk scanning with run_many()
run_many() scans multiple targets sequentially and propagates all callbacks to each per-target scan:
import asyncio
from plecost import Scanner, ScanOptions
findings_by_url: dict[str, list] = {}
def on_finding(finding):
findings_by_url.setdefault(finding.module, []).append(finding.id)
async def main():
opts = ScanOptions(
url="https://placeholder.example", # overridden per target
modules=["fingerprint", "plugins", "cves"],
brute_force=True,
brute_force_use_defaults=True,
)
scanner = Scanner(opts, on_finding=on_finding)
results = await scanner.run_many([
"https://site1.example.com",
"https://site2.example.com",
])
for result in results:
print(f"{result.url}: {result.summary.critical} critical findings")
asyncio.run(main())
Celery workers
from celery import Celery
from plecost import Scanner, ScanOptions
import asyncio
app = Celery("tasks")
@app.task
def scan_wordpress(url: str) -> dict:
opts = ScanOptions(url=url, modules=["fingerprint", "plugins", "cves"])
result = asyncio.run(Scanner(opts).run())
return {
"url": result.url,
"critical": result.summary.critical,
"findings": [f.id for f in result.findings],
}
Environment Variables
| Variable | Description | Used by |
|---|---|---|
PLECOST_DB_URL |
Database URL (SQLite or PostgreSQL) | update-db, scan |
PLECOST_TIMEOUT |
Request timeout in seconds | scan |
PLECOST_OUTPUT |
JSON output file path | scan |
GITHUB_TOKEN |
GitHub token to avoid download rate limiting | update-db |
Architecture
Modules without interdependencies run concurrently from the start. cves waits for plugins and themes to complete so it has a full list of installed software to match against the CVE database.
Troubleshooting
"CVE database not found"
The local database hasn't been downloaded yet:
plecost update-db
Target returns 429 (rate limiting)
# Reduce concurrency
plecost scan https://target.com --concurrency 3
# Or use stealth mode (includes automatic pacing)
plecost scan https://target.com --stealth
SSL certificate errors
plecost scan https://target.com --no-verify-ssl
Only use
--no-verify-sslin controlled environments.
Target returns 403 (scanner blocked)
Plecost detects this automatically on the pre-flight probe and aborts cleanly with finding PC-PRE-001. Try a different IP, a proxy, or a different User-Agent:
plecost scan https://target.com --proxy http://127.0.0.1:8080
plecost scan https://target.com --random-user-agent
WordPress not detected
plecost scan https://target.com --force
Local Test Environment
A self-contained Docker Compose environment — Damn Vulnerable WordPress (DVWP) — is included for local testing and development. It spins up a fully configured WordPress instance with a curated set of outdated, intentionally vulnerable plugins.
Located at tests/dvwp/.
Start
cd tests/dvwp
docker compose up -d
docker compose logs wpcli -f # watch setup (~60s), wait for plugin table
Once wpcli exits, the environment is ready:
| URL | Credentials |
|---|---|
| http://localhost:8765 | — |
| http://localhost:8765/wp-admin | admin / admin |
Pre-installed vulnerable plugins
| Plugin | Version | CVE |
|---|---|---|
| wpDiscuz | 7.0.4 | CVE-2020-24186 — unauthenticated RCE via file upload (CVSS 9.8) |
| Contact Form 7 | 5.3.1 | CVE-2020-35489 — unrestricted file upload |
| WooCommerce | 5.0.0 | CVE-2021-32790 — multiple |
| WooCommerce Payments | 3.9.0 | CVE-2023-28121 — unauthenticated privilege escalation (CVSS 9.8) |
| WooCommerce Stripe Gateway | 4.3.0 | CVE-2019-15826 — order information disclosure |
| Easy Digital Downloads | 2.11.5 | CVE-2021-39351 — stored XSS |
| Give – Donation Plugin | 2.10.3 | CVE-2021-34634 — SQL injection |
| YITH WooCommerce Wishlist | 2.2.9 | CVE-2021-24987 — stored XSS |
| Ninja Forms | 3.4.34.2 | CVE-2021-34648 — unauthenticated email injection |
| Duplicator | 1.3.26 | CVE-2020-11738 — path traversal |
| Loginizer | 1.6.3 | CVE-2020-27615 — SQL injection |
| Elementor | 3.1.2 | CVE-2022-1329 — authenticated RCE |
| WP Super Cache | 1.7.1 | CVE-2021-33203 — authenticated XSS |
| Wordfence | 7.5.0 | CVE-2021-24875 — reflected XSS |
Run plecost against it
plecost scan http://localhost:8765 -v
plecost scan http://localhost:8765 --deep -v
Reset
docker compose down -v && docker compose up -d
See tests/dvwp/README.md for full details.
Warning: For local/isolated use only. Never expose to the internet.
License
Plecost is distributed under the PolyForm Noncommercial License 1.0.0.
Free for: personal security research, internal corporate audits, academic and educational use, open source projects, charitable and government organizations.
Requires a commercial license for: scanning-as-a-service, inclusion in a commercial product, or any use generating direct or indirect revenue.
For commercial licensing: cr0hn@cr0hn.com (Dani) · ffranz@mrlooquer.com (Fran)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plecost-4.8.0.tar.gz.
File metadata
- Download URL: plecost-4.8.0.tar.gz
- Upload date:
- Size: 262.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bc87948c9e4c2850a55e82a4be1742684dca17f347bdc27ae02cca932be88b0
|
|
| MD5 |
6fc175ea9d473924ef469618d698e659
|
|
| BLAKE2b-256 |
41c74275b1908564a30d60feb9742c5c494408328efb1ca2f5f4c52a6f861cd7
|
File details
Details for the file plecost-4.8.0-py3-none-any.whl.
File metadata
- Download URL: plecost-4.8.0-py3-none-any.whl
- Upload date:
- Size: 155.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b7c80440d57c34aef27cbbff6fe7f809ef7846c8ac4219d3fe0bb7f867b1878
|
|
| MD5 |
52d2ddd801ab79ced2d70bb817c873b4
|
|
| BLAKE2b-256 |
cd1a9560f9fd11104cd635d5237a0180ad7f49aea163e6f62c77d6203358fad9
|