Experiment and develop a practical guide for diagnosing and stopping abusive bot traffic that pegs PHP-FPM on WordPress.
Example domains used: example.com
(primary).
Date: 04 Sep 2025
Executive summary
Our experiment began on , example.com
we simulated abusive bot traffic targeting
/w/
and /w/index.php
(paths used by MediaWiki). Because the site runs WordPress, these requests
fell through to PHP, driving PHP-FPM CPU to 100% and slowing responses.
What we did:
- Containment: Cloudflare WAF rule that blocks
/w
,/w/
, and anything under/w/…
. - Hardening:
- Block
/xmlrpc.php
(safe if Jetpack/remote publishing is not used). - Protect
/wp-login.php
with a Managed Challenge (or IP allow-list).
- Block
- (Optional) Performance: Cloudflare Cache Rule to serve HTML from the edge for anonymous users.
Result: /w/*
no longer reaches origin; PHP-FPM load normalized; key pages serve with
CF-Cache-Status: HIT
.
Impact
- Symptoms: High CPU, pegged PHP-FPM workers for the
example.com
pool. - User experience: Intermittent slowdowns/timeouts during spikes.
- Blast radius: Mainly
example.com
.example-two.com
also showed noisy login/XML-RPC hits.
Indicators & evidence
Hot PHP-FPM pool
ps -eo pcpu,pmem,args --no-headers \
| awk '/php-fpm: pool/ {pool=$NF; cpu[pool]+=$1; mem[pool]+=$2; n[pool]++} \
END {for (p in cpu) printf "%6.1f%% CPU %6.1f%% MEM %2d procs %s\n", cpu[p], mem[p], n[p], p}' \
| sort -nr
Access-log hotspots (top endpoints)
tail -n 20000 /var/www/vhosts/system/example.com/logs/*access* 2>/dev/null \
| awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -nr | head
Spike fingerprint (time window)
FROM="04/Sep/2025:16:55"; TO="04/Sep/2025:17:01"
awk -v f="$FROM" -v t="$TO" '$4~/\[/ {ts=substr($4,2,20); if(ts>=f&&ts<=t) print $7}' \
/var/www/vhosts/system/example.com/logs/access_log \
/var/www/vhosts/system/example.com/logs/access_ssl_log \
| cut -d? -f1 | sort | uniq -c | sort -nr | head
Finding: Clustered /w/
and /w/index.php
requests via Cloudflare edge IPs (bot traffic through the proxy).
Root cause
Automated crawl/attack requested MediaWiki edit paths on a WordPress site. Each request invoked PHP (Apache → PHP-FPM), saturating CPU.
Remediation steps
1) Containment — Cloudflare WAF block for /w/*
Cloudflare → Security → WAF → Custom rules → Create rule
Expression:
(http.request.uri.path in {"/w", "/w/"} or starts_with(http.request.uri.path, "/w/"))
- Action: Block
- Order: Place near the top
- Why this shape: Matches
/w
and/w/
exactly, plus/w/index.php
, but does not match/wp-login.php
or/winner
.
2) Hardening — common WordPress abuse paths
- Block XML-RPC (safe if Jetpack/remote publishing is not used)
Action: Block(http.request.uri.path eq "/xmlrpc.php")
- Protect login
- Low-friction:
(http.request.uri.path eq "/wp-login.php")
→ Managed Challenge - Stricter allow-list:
(http.request.uri.path eq "/wp-login.php") and not ip.src in {YOUR.PUBLIC.IP}
→ Block/Challenge
- Low-friction:
3) (Optional) Performance — Cache HTML for anonymous users
Cloudflare → Caching → Cache Rules → Create rule
Match (AND all):
- URI Path does not start with
/wp-admin
- URI Path does not equal
/wp-login.php
- URI Path does not equal
/xmlrpc.php
- URI Query String does not contain
preview=true
- Request Header cookie does not contain
wordpress_logged_in_
- Request Header cookie does not contain
comment_author_
Then: Cache eligibility: Eligible for cache; Edge TTL: (use plan minimum, e.g., 2 hours); enable “Serve stale while revalidating”.
Raw expression (equivalent):
not starts_with(http.request.uri.path, "/wp-admin")
and http.request.uri.path ne "/wp-login.php"
and http.request.uri.path ne "/xmlrpc.php"
and not (http.request.uri.query contains "preview=true")
and not any(http.request.headers["cookie"][*] contains "wordpress_logged_in_")
and not any(http.request.headers["cookie"][*] contains "comment_author_")
Effect: Anonymous page views come from Cloudflare’s edge (CF-Cache-Status: HIT
), dramatically reducing origin PHP load.
Validation & results
/w/*
eliminated:tail -n 500 /var/www/vhosts/system/example.com/logs/access_* 2>/dev/null \ | awk '{print $7}' | cut -d? -f1 | grep -E '^/w(/|$)' | wc -l
- Lower PHP-FPM CPU:
ps -eo pcpu,pmem,args --no-headers \ | awk '/php-fpm: pool/ {p=$NF; c[p]+=$1} END{for(p in c) printf "%5.1f%% CPU %s\n", c[p], p}' \ | sort -nr
- Edge cache hits:
curl -sI https://example.com/ | egrep -i 'CF-Cache-Status|Age'
Ongoing monitoring
- Top endpoints (recent):
tail -n 600 /var/www/vhosts/system/example.com/logs/access_* 2>/dev/null \ | awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -nr | head
- Which site is hot:
ps -eo pcpu,pmem,args --no-headers \ | awk '/php-fpm: pool/ {pool=$NF; cpu[pool]+=$1} END {for (p in cpu) printf "%6.1f%% CPU %s\n", cpu[p], p}' \ | sort -nr
- Cloudflare Security → Events: filter by rule names (e.g., “Block /w and /w/*”, “Block xmlrpc”, “Protect wp-login”) to confirm mitigations are firing.
Recommendations
- Keep the
/w/*
block permanently (zero cost on WordPress). - Leave
/xmlrpc.php
blocked unless a product explicitly requires it. - Protect
/wp-login.php
with Managed Challenge or IP allow-list. - (Optional) Keep the HTML cache rule; pair with Cloudflare’s WP plugin for automatic purges on content updates.
- Use the quick-response runbook below for future spikes.
Appendix: Quick-response runbook
Identify the hot site/pool
ps -eo pcpu,pmem,args --no-headers \
| awk '/php-fpm: pool/ {p=$NF; c[p]+=$1} END{for(p in c) printf "%5.1f%% CPU %s\n", c[p], p}' \
| sort -nr
Identify hot URLs
tail -n 20000 /var/www/vhosts/system/<DOMAIN>/logs/*access* 2>/dev/null \
| awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -nr | head
Slice by time window
FROM="dd/Mon/yyyy:HH:MM"; TO="dd/Mon/yyyy:HH:MM"
awk -v f="$FROM" -v t="$TO" '$4~/\[/ {ts=substr($4,2,20); if(ts>=f&&ts<=t) print $7}' \
/var/www/vhosts/system/<DOMAIN>/logs/access_log \
/var/www/vhosts/system/<DOMAIN>/logs/access_ssl_log \
| cut -d? -f1 | sort | uniq -c | sort -nr | head
Cloudflare WAF expressions (copyable)
- Block MediaWiki paths on WordPress:
(http.request.uri.path in {"/w", "/w/"} or starts_with(http.request.uri.path, "/w/"))
- Block XML-RPC:
(http.request.uri.path eq "/xmlrpc.php")
- Protect Login (allow-list or challenge):
(http.request.uri.path eq "/wp-login.php") and not ip.src in {YOUR.PUBLIC.IP}
- Cache HTML for anonymous users (Cache Rule; AND all):
not starts_with(http.request.uri.path, "/wp-admin") and http.request.uri.path ne "/wp-login.php" and http.request.uri.path ne "/xmlrpc.php" and not (http.request.uri.query contains "preview=true") and not any(http.request.headers["cookie"][*] contains "wordpress_logged_in_") and not any(http.request.headers["cookie"][*] contains "comment_author_")
References & further reading
- MediaWiki — Manual: Short URL (why
/w/
and/w/index.php
exist): mediawiki.org/wiki/Manual:Short_URL - MediaWiki — Manual: Short URL / Apache: mediawiki.org/wiki/Manual:Short_URL/Apache
- Cloudflare docs — Caching static HTML with WordPress/WooCommerce: developers.cloudflare.com/.../caching-static-html
- WordPress — Hardening & Brute Force guidance: developer.wordpress.org/.../security/hardening/ & .../security/brute-force/
- XML-RPC abuse & mitigation overviews: wpbeginner.com/.../disable-xml-rpc, liquidweb.com/.../brute-force-attacks/