Lab Test: Bot Traffic & Mitigation Playbook Development (WordPress + Cloudflare)

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).
  • (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)
    (http.request.uri.path eq "/xmlrpc.php")
    Action: Block
  • 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

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

  1. Keep the /w/* block permanently (zero cost on WordPress).
  2. Leave /xmlrpc.php blocked unless a product explicitly requires it.
  3. Protect /wp-login.php with Managed Challenge or IP allow-list.
  4. (Optional) Keep the HTML cache rule; pair with Cloudflare’s WP plugin for automatic purges on content updates.
  5. 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