{"id":18,"date":"2025-09-05T00:07:39","date_gmt":"2025-09-05T00:07:39","guid":{"rendered":"https:\/\/progresstechnologies.com\/?p=18"},"modified":"2025-09-05T00:09:13","modified_gmt":"2025-09-05T00:09:13","slug":"lab-test-bot-traffic-mitigation-playbook-development-wordpress-cloudflare","status":"publish","type":"post","link":"https:\/\/progresstechnologies.com\/?p=18","title":{"rendered":"Lab Test: Bot Traffic &amp; Mitigation Playbook Development (WordPress + Cloudflare)"},"content":{"rendered":"\n<article itemscope itemtype=\"https:\/\/schema.org\/TechArticle\">\n  <header>\n    <p itemprop=\"description\">\n      Experiment and develop a practical guide for diagnosing and stopping abusive bot traffic that pegs PHP-FPM on WordPress. \n      Example domains used: <code>example.com<\/code> (primary).\n    <\/p>\n    <p><strong>Date:<\/strong> 04 Sep 2025<\/p>\n  <\/header>\n\n  <!-- Table of contents -->\n  <nav aria-label=\"Table of contents\">\n    <h2>Contents<\/h2>\n    <ol>\n      <li><a href=\"#summary\">Executive summary<\/a><\/li>\n      <li><a href=\"#impact\">Impact<\/a><\/li>\n      <li><a href=\"#evidence\">Indicators &amp; evidence<\/a><\/li>\n      <li><a href=\"#root-cause\">Root cause<\/a><\/li>\n      <li><a href=\"#remediation\">Remediation steps<\/a>\n        <ol>\n          <li><a href=\"#containment\">Containment: Block <code>\/w\/*<\/code><\/a><\/li>\n          <li><a href=\"#hardening\">Hardening: Login &amp; XML-RPC<\/a><\/li>\n          <li><a href=\"#caching\">(Optional) Performance: Cache HTML for anonymous users<\/a><\/li>\n        <\/ol>\n      <\/li>\n      <li><a href=\"#validation\">Validation &amp; results<\/a><\/li>\n      <li><a href=\"#monitoring\">Ongoing monitoring<\/a><\/li>\n      <li><a href=\"#recommendations\">Recommendations<\/a><\/li>\n      <li><a href=\"#runbook\">Appendix: Quick-response runbook<\/a><\/li>\n      <li><a href=\"#references\">References<\/a><\/li>\n    <\/ol>\n  <\/nav>\n\n  <section id=\"summary\">\n    <h2>Executive summary<\/h2>\n    <p>\n      Our experiment began on <time datetime=\"2025-09-04\">04 Sep 2025<\/time>, <code>example.com<\/code> we simulated abusive bot traffic targeting \n      <strong><code>\/w\/<\/code> and <code>\/w\/index.php<\/code><\/strong> (paths used by MediaWiki). Because the site runs WordPress, these requests\n      fell through to PHP, driving PHP-FPM CPU to 100% and slowing responses.\n    <\/p>\n    <p><strong>What we did:<\/strong><\/p>\n    <ul>\n      <li><strong>Containment:<\/strong> Cloudflare WAF rule that blocks <code>\/w<\/code>, <code>\/w\/<\/code>, and anything under <code>\/w\/\u2026<\/code>.<\/li>\n      <li><strong>Hardening:<\/strong> \n        <ul>\n          <li>Block <code>\/xmlrpc.php<\/code> (safe if Jetpack\/remote publishing is not used).<\/li>\n          <li>Protect <code>\/wp-login.php<\/code> with a Managed Challenge (or IP allow-list).<\/li>\n        <\/ul>\n      <\/li>\n      <li><strong>(Optional) Performance:<\/strong> Cloudflare Cache Rule to serve HTML from the edge for anonymous users.<\/li>\n    <\/ul>\n    <p><strong>Result:<\/strong> <code>\/w\/*<\/code> no longer reaches origin; PHP-FPM load normalized; key pages serve with \n      <code>CF-Cache-Status: HIT<\/code>.<\/p>\n  <\/section>\n\n  <section id=\"impact\">\n    <h2>Impact<\/h2>\n    <ul>\n      <li><strong>Symptoms:<\/strong> High CPU, pegged PHP-FPM workers for the <code>example.com<\/code> pool.<\/li>\n      <li><strong>User experience:<\/strong> Intermittent slowdowns\/timeouts during spikes.<\/li>\n      <li><strong>Blast radius:<\/strong> Mainly <code>example.com<\/code>. <code>example-two.com<\/code> also showed noisy login\/XML-RPC hits.<\/li>\n    <\/ul>\n  <\/section>\n\n  <section id=\"evidence\">\n    <h2>Indicators &amp; evidence<\/h2>\n\n    <h3>Hot PHP-FPM pool<\/h3>\n    <pre><code>ps -eo pcpu,pmem,args --no-headers \\\n| awk '\/php-fpm: pool\/ {pool=$NF; cpu[pool]+=$1; mem[pool]+=$2; n[pool]++} \\\n       END {for (p in cpu) printf \"%6.1f%% CPU  %6.1f%% MEM  %2d procs  %s\\n\", cpu[p], mem[p], n[p], p}' \\\n| sort -nr<\/code><\/pre>\n\n    <h3>Access-log hotspots (top endpoints)<\/h3>\n    <pre><code>tail -n 20000 \/var\/www\/vhosts\/system\/example.com\/logs\/*access* 2>\/dev\/null \\\n| awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -nr | head<\/code><\/pre>\n\n    <h3>Spike fingerprint (time window)<\/h3>\n    <pre><code>FROM=\"04\/Sep\/2025:16:55\"; TO=\"04\/Sep\/2025:17:01\"\nawk -v f=\"$FROM\" -v t=\"$TO\" '$4~\/\\[\/ {ts=substr($4,2,20); if(ts>=f&&ts<=t) print $7}' \\\n  \/var\/www\/vhosts\/system\/example.com\/logs\/access_log \\\n  \/var\/www\/vhosts\/system\/example.com\/logs\/access_ssl_log \\\n| cut -d? -f1 | sort | uniq -c | sort -nr | head<\/code><\/pre>\n\n    <p><strong>Finding:<\/strong> Clustered <code>\/w\/<\/code> and <code>\/w\/index.php<\/code> requests via Cloudflare edge IPs (bot traffic through the proxy).<\/p>\n  <\/section>\n\n  <section id=\"root-cause\">\n    <h2>Root cause<\/h2>\n    <p>Automated crawl\/attack requested MediaWiki edit paths on a WordPress site. Each request invoked PHP (Apache \u2192 PHP-FPM), saturating CPU.<\/p>\n  <\/section>\n\n  <section id=\"remediation\">\n    <h2>Remediation steps<\/h2>\n\n    <section id=\"containment\">\n      <h3>1) Containment \u2014 Cloudflare WAF block for <code>\/w\/*<\/code><\/h3>\n      <p><strong>Cloudflare &rarr; Security &rarr; WAF &rarr; Custom rules &rarr; Create rule<\/strong><\/p>\n      <p><strong>Expression:<\/strong><\/p>\n      <pre><code>(http.request.uri.path in {\"\/w\", \"\/w\/\"} or starts_with(http.request.uri.path, \"\/w\/\"))<\/code><\/pre>\n      <ul>\n        <li><strong>Action:<\/strong> Block<\/li>\n        <li><strong>Order:<\/strong> Place near the top<\/li>\n        <li><strong>Why this shape:<\/strong> Matches <code>\/w<\/code> and <code>\/w\/<\/code> exactly, plus <code>\/w\/index.php<\/code>, but does <em>not<\/em> match <code>\/wp-login.php<\/code> or <code>\/winner<\/code>.<\/li>\n      <\/ul>\n    <\/section>\n\n    <section id=\"hardening\">\n      <h3>2) Hardening \u2014 common WordPress abuse paths<\/h3>\n      <ul>\n        <li><strong>Block XML-RPC<\/strong> (safe if Jetpack\/remote publishing is not used)\n          <pre><code>(http.request.uri.path eq \"\/xmlrpc.php\")<\/code><\/pre>\n          <em>Action:<\/em> Block\n        <\/li>\n        <li><strong>Protect login<\/strong>\n          <ul>\n            <li><em>Low-friction:<\/em> <code>(http.request.uri.path eq \"\/wp-login.php\")<\/code> \u2192 Managed Challenge<\/li>\n            <li><em>Stricter allow-list:<\/em> <code>(http.request.uri.path eq \"\/wp-login.php\") and not ip.src in {YOUR.PUBLIC.IP}<\/code> \u2192 Block\/Challenge<\/li>\n          <\/ul>\n        <\/li>\n      <\/ul>\n    <\/section>\n\n    <section id=\"caching\">\n      <h3>3) (Optional) Performance \u2014 Cache HTML for anonymous users<\/h3>\n      <p><strong>Cloudflare &rarr; Caching &rarr; Cache Rules &rarr; Create rule<\/strong><\/p>\n      <p><strong>Match (AND all):<\/strong><\/p>\n      <ul>\n        <li>URI Path <strong>does not start with<\/strong> <code>\/wp-admin<\/code><\/li>\n        <li>URI Path <strong>does not equal<\/strong> <code>\/wp-login.php<\/code><\/li>\n        <li>URI Path <strong>does not equal<\/strong> <code>\/xmlrpc.php<\/code><\/li>\n        <li>URI Query String <strong>does not contain<\/strong> <code>preview=true<\/code><\/li>\n        <li>Request Header <strong>cookie<\/strong> <strong>does not contain<\/strong> <code>wordpress_logged_in_<\/code><\/li>\n        <li>Request Header <strong>cookie<\/strong> <strong>does not contain<\/strong> <code>comment_author_<\/code><\/li>\n      <\/ul>\n      <p><strong>Then:<\/strong> <em>Cache eligibility:<\/em> Eligible for cache; <em>Edge TTL:<\/em> (use plan minimum, e.g., 2 hours); enable \u201cServe stale while revalidating\u201d.<\/p>\n      <p><strong>Raw expression (equivalent):<\/strong><\/p>\n      <pre><code>not starts_with(http.request.uri.path, \"\/wp-admin\")\nand http.request.uri.path ne \"\/wp-login.php\"\nand http.request.uri.path ne \"\/xmlrpc.php\"\nand not (http.request.uri.query contains \"preview=true\")\nand not any(http.request.headers[\"cookie\"][*] contains \"wordpress_logged_in_\")\nand not any(http.request.headers[\"cookie\"][*] contains \"comment_author_\")<\/code><\/pre>\n      <p><em>Effect:<\/em> Anonymous page views come from Cloudflare\u2019s edge (<code>CF-Cache-Status: HIT<\/code>), dramatically reducing origin PHP load.<\/p>\n    <\/section>\n  <\/section>\n\n  <section id=\"validation\">\n    <h2>Validation &amp; results<\/h2>\n    <ul>\n      <li><strong><code>\/w\/*<\/code> eliminated:<\/strong>\n        <pre><code>tail -n 500 \/var\/www\/vhosts\/system\/example.com\/logs\/access_* 2>\/dev\/null \\\n| awk '{print $7}' | cut -d? -f1 | grep -E '^\/w(\/|$)' | wc -l  <!-- expect 0 or near-0 --><\/code><\/pre>\n      <\/li>\n      <li><strong>Lower PHP-FPM CPU:<\/strong>\n        <pre><code>ps -eo pcpu,pmem,args --no-headers \\\n| awk '\/php-fpm: pool\/ {p=$NF; c[p]+=$1} END{for(p in c) printf \"%5.1f%% CPU  %s\\n\", c[p], p}' \\\n| sort -nr<\/code><\/pre>\n      <\/li>\n      <li><strong>Edge cache hits:<\/strong>\n        <pre><code>curl -sI https:\/\/example.com\/ | egrep -i 'CF-Cache-Status|Age'  <!-- expect: CF-Cache-Status: HIT --><\/code><\/pre>\n      <\/li>\n    <\/ul>\n  <\/section>\n\n  <section id=\"monitoring\">\n    <h2>Ongoing monitoring<\/h2>\n    <ul>\n      <li><strong>Top endpoints (recent):<\/strong>\n        <pre><code>tail -n 600 \/var\/www\/vhosts\/system\/example.com\/logs\/access_* 2>\/dev\/null \\\n| awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -nr | head<\/code><\/pre>\n      <\/li>\n      <li><strong>Which site is hot:<\/strong>\n        <pre><code>ps -eo pcpu,pmem,args --no-headers \\\n| awk '\/php-fpm: pool\/ {pool=$NF; cpu[pool]+=$1} END {for (p in cpu) printf \"%6.1f%% CPU  %s\\n\", cpu[p], p}' \\\n| sort -nr<\/code><\/pre>\n      <\/li>\n      <li><strong>Cloudflare Security \u2192 Events:<\/strong> filter by rule names (e.g., \u201cBlock \/w and \/w\/*\u201d, \u201cBlock xmlrpc\u201d, \u201cProtect wp-login\u201d) to confirm mitigations are firing.<\/li>\n    <\/ul>\n  <\/section>\n\n  <section id=\"recommendations\">\n    <h2>Recommendations<\/h2>\n    <ol>\n      <li>Keep the <code>\/w\/*<\/code> block permanently (zero cost on WordPress).<\/li>\n      <li>Leave <code>\/xmlrpc.php<\/code> blocked unless a product explicitly requires it.<\/li>\n      <li>Protect <code>\/wp-login.php<\/code> with Managed Challenge or IP allow-list.<\/li>\n      <li>(Optional) Keep the HTML cache rule; pair with Cloudflare\u2019s WP plugin for automatic purges on content updates.<\/li>\n      <li>Use the quick-response runbook below for future spikes.<\/li>\n    <\/ol>\n  <\/section>\n\n  <section id=\"runbook\">\n    <h2>Appendix: Quick-response runbook<\/h2>\n    <h3>Identify the hot site\/pool<\/h3>\n    <pre><code>ps -eo pcpu,pmem,args --no-headers \\\n| awk '\/php-fpm: pool\/ {p=$NF; c[p]+=$1} END{for(p in c) printf \"%5.1f%% CPU  %s\\n\", c[p], p}' \\\n| sort -nr<\/code><\/pre>\n\n    <h3>Identify hot URLs<\/h3>\n    <pre><code>tail -n 20000 \/var\/www\/vhosts\/system\/&lt;DOMAIN&gt;\/logs\/*access* 2>\/dev\/null \\\n| awk '{print $7}' | cut -d? -f1 | sort | uniq -c | sort -nr | head<\/code><\/pre>\n\n    <h3>Slice by time window<\/h3>\n    <pre><code>FROM=\"dd\/Mon\/yyyy:HH:MM\"; TO=\"dd\/Mon\/yyyy:HH:MM\"\nawk -v f=\"$FROM\" -v t=\"$TO\" '$4~\/\\[\/ {ts=substr($4,2,20); if(ts>=f&&ts<=t) print $7}' \\\n  \/var\/www\/vhosts\/system\/&lt;DOMAIN&gt;\/logs\/access_log \\\n  \/var\/www\/vhosts\/system\/&lt;DOMAIN&gt;\/logs\/access_ssl_log \\\n| cut -d? -f1 | sort | uniq -c | sort -nr | head<\/code><\/pre>\n\n    <h3>Cloudflare WAF expressions (copyable)<\/h3>\n    <ul>\n      <li>Block MediaWiki paths on WordPress:\n        <pre><code>(http.request.uri.path in {\"\/w\", \"\/w\/\"} or starts_with(http.request.uri.path, \"\/w\/\"))<\/code><\/pre>\n      <\/li>\n      <li>Block XML-RPC:\n        <pre><code>(http.request.uri.path eq \"\/xmlrpc.php\")<\/code><\/pre>\n      <\/li>\n      <li>Protect Login (allow-list or challenge):\n        <pre><code>(http.request.uri.path eq \"\/wp-login.php\") and not ip.src in {YOUR.PUBLIC.IP}<\/code><\/pre>\n      <\/li>\n      <li>Cache HTML for anonymous users (Cache Rule; AND all):\n        <pre><code>not starts_with(http.request.uri.path, \"\/wp-admin\")\nand http.request.uri.path ne \"\/wp-login.php\"\nand http.request.uri.path ne \"\/xmlrpc.php\"\nand not (http.request.uri.query contains \"preview=true\")\nand not any(http.request.headers[\"cookie\"][*] contains \"wordpress_logged_in_\")\nand not any(http.request.headers[\"cookie\"][*] contains \"comment_author_\")<\/code><\/pre>\n      <\/li>\n    <\/ul>\n  <\/section>\n\n  <section id=\"references\">\n    <h2>References &amp; further reading<\/h2>\n    <ul>\n      <li>MediaWiki \u2014 Manual: Short URL (why <code>\/w\/<\/code> and <code>\/w\/index.php<\/code> exist):\n        <a href=\"https:\/\/www.mediawiki.org\/wiki\/Manual:Short_URL\" rel=\"nofollow\">mediawiki.org\/wiki\/Manual:Short_URL<\/a>\n      <\/li>\n      <li>MediaWiki \u2014 Manual: Short URL \/ Apache:\n        <a href=\"https:\/\/www.mediawiki.org\/wiki\/Manual:Short_URL\/Apache\" rel=\"nofollow\">mediawiki.org\/wiki\/Manual:Short_URL\/Apache<\/a>\n      <\/li>\n      <li>Cloudflare docs \u2014 Caching static HTML with WordPress\/WooCommerce:\n        <a href=\"https:\/\/developers.cloudflare.com\/support\/third-party-software\/content-management-system-cms\/caching-static-html-with-wordpresswoocommerce\/\" rel=\"nofollow\">developers.cloudflare.com\/...\/caching-static-html<\/a>\n      <\/li>\n      <li>WordPress \u2014 Hardening &amp; Brute Force guidance:\n        <a href=\"https:\/\/developer.wordpress.org\/advanced-administration\/security\/hardening\/\" rel=\"nofollow\">developer.wordpress.org\/...\/security\/hardening\/<\/a> &amp;\n        <a href=\"https:\/\/developer.wordpress.org\/advanced-administration\/security\/brute-force\/\" rel=\"nofollow\">...\/security\/brute-force\/<\/a>\n      <\/li>\n      <li>XML-RPC abuse &amp; mitigation overviews:\n        <a href=\"https:\/\/www.wpbeginner.com\/plugins\/how-to-disable-xml-rpc-in-wordpress\/\" rel=\"nofollow\">wpbeginner.com\/...\/disable-xml-rpc<\/a>,\n        <a href=\"https:\/\/www.liquidweb.com\/help-docs\/cms\/wordpress\/mitigating-common-wordpress-brute-force-attacks\/\" rel=\"nofollow\">liquidweb.com\/...\/brute-force-attacks\/<\/a>\n      <\/li>\n    <\/ul>\n  <\/section>\n\n  <!-- Lightweight structured data for SEO -->\n  <script type=\"application\/ld+json\">\n  {\n    \"@context\": \"https:\/\/schema.org\",\n    \"@type\": \"TechArticle\",\n    \"headline\": \"Bot Traffic Incident & Mitigation Playbook (WordPress + Cloudflare)\",\n    \"about\": [\"WordPress security\", \"Cloudflare WAF\", \"PHP-FPM performance\", \"Bot mitigation\"],\n    \"datePublished\": \"2025-09-04\",\n    \"dateModified\": \"2025-09-04\",\n    \"author\": { \"@type\": \"Organization\", \"name\": \"Example Team\" },\n    \"publisher\": { \"@type\": \"Organization\", \"name\": \"Example Org\" }\n  }\n  <\/script>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>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 Contents Executive summary Impact Indicators &amp; evidence Root cause Remediation steps Containment: Block \/w\/* Hardening: Login &amp; XML-RPC (Optional) Performance: Cache HTML for anonymous users Validation &amp; results [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-18","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=\/wp\/v2\/posts\/18","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=18"}],"version-history":[{"count":4,"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=\/wp\/v2\/posts\/18\/revisions"}],"predecessor-version":[{"id":22,"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=\/wp\/v2\/posts\/18\/revisions\/22"}],"wp:attachment":[{"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=18"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=18"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/progresstechnologies.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=18"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}