Skip to content

Instantly share code, notes, and snippets.

@rogulia
Last active April 14, 2026 14:33
Show Gist options
  • Select an option

  • Save rogulia/5ae4d9fd39b2994303a1a8b6e2b226ce to your computer and use it in GitHub Desktop.

Select an option

Save rogulia/5ae4d9fd39b2994303a1a8b6e2b226ce to your computer and use it in GitHub Desktop.
Cloudflare WAF custom rule to block scanner bots — works on the free plan (uses 'contains', not 'starts_with')
#!/bin/bash
# Cloudflare WAF — Block scanner bots (free plan compatible)
# Usage: CF_ZONE_ID=xxx CF_AUTH_TOKEN=yyy bash block-bots.sh
#
# WARNING: This replaces ALL custom WAF rules in the zone.
# If you have existing rules, add them to the "rules" array below.
#
# Zone ID: Cloudflare dashboard → your zone → Overview → right sidebar
# API Token: dash.cloudflare.com/profile/api-tokens (Zone:Edit permission)
set -e
if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_AUTH_TOKEN" ]; then
echo "Error: CF_ZONE_ID and CF_AUTH_TOKEN must be set"
echo "Usage: CF_ZONE_ID=xxx CF_AUTH_TOKEN=yyy bash block-bots.sh"
exit 1
fi
echo "Deploying bot blocking rule to zone $CF_ZONE_ID..."
curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
-H "Authorization: Bearer $CF_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{
"description": "Block scanner bots",
"expression": "(http.request.uri.path contains \"/.env\") or (http.request.uri.path contains \"/.git/\") or (http.request.uri.path contains \"/.aws/\") or (http.request.uri.path contains \"/actuator/\") or (http.request.uri.path contains \"/wp-admin/\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/xmlrpc\") or (http.request.uri.path contains \"/phpmyadmin/\") or (http.request.uri.path contains \"/laravel/\") or (http.request.uri.path contains \"/backend/\") or (http.request.uri.path eq \"/config.php\") or (http.request.uri.path eq \"/server-status\")",
"action": "block"
}
]
}' | jq '{success: .success, rule_id: .result.rules[0].id, errors: .errors}'
echo "Done. Verify: curl -I https://yourdomain.com/.env # should return 403"

Cloudflare WAF — Free Bot Blocking Rule

Blocks /.env, /.git/, /wp-admin/ and more. Works on the free plan — no upgrade needed. Uses contains, not starts_with (parse error). Deploy via dashboard or run block-bots.sh.

Cloudflare WAF Free Bot Blocking

What It Blocks

Path What attackers look for
/.env Environment file — API keys, database passwords, secrets
/.git/ Git repository — sometimes contains credentials, reveals codebase structure
/.aws/ AWS credentials file — direct cloud access
/actuator/ Spring Boot management endpoints — often unauthenticated
/wp-admin/ WordPress admin panel — brute-force login target
/wp-login WordPress login — credential stuffing
/xmlrpc WordPress XML-RPC — amplification attacks, credential testing
/phpmyadmin/ Database management UI — direct database access
/laravel/ Laravel framework paths — debug mode exposure
/backend/ Generic admin paths — admin panel probing
/config.php Configuration files — credentials
/server-status Apache server status — internal metrics exposure

These scans are automated and indiscriminate. Bots probe every public IP regardless of what your app actually is.

Free Plan Gotcha: Why contains, Not starts_with

If you search for Cloudflare bot blocking rules, most examples use starts_with. This does not work in custom WAF rules on any plan:

# This throws a parse error:
(http.request.uri.path starts_with "/.env")
# Filter parsing error (1:24): expected ComparisonOp

matches (regex) requires a Business plan or WAF Advanced:

# This throws:
# not entitled: the use of operator Matches is not allowed,
# a Business plan or a WAF Advanced plan is required

Only contains and eq work for path matching on the free plan. This is not documented clearly anywhere. The rule in this repo uses contains throughout, which catches everything starts_with would have caught.

The Expression

(http.request.uri.path contains "/.env") or (http.request.uri.path contains "/.git/") or (http.request.uri.path contains "/.aws/") or (http.request.uri.path contains "/actuator/") or (http.request.uri.path contains "/wp-admin/") or (http.request.uri.path contains "/wp-login") or (http.request.uri.path contains "/xmlrpc") or (http.request.uri.path contains "/phpmyadmin/") or (http.request.uri.path contains "/laravel/") or (http.request.uri.path contains "/backend/") or (http.request.uri.path eq "/config.php") or (http.request.uri.path eq "/server-status")

Action: Block

Option 1: Cloudflare Dashboard (2 minutes)

  1. Log into Cloudflare → select your zone
  2. Go to Security → WAF → Custom rules
  3. Click Create rule
  4. Name it anything ("Block scanner bots")
  5. Switch expression editor to Edit expression (text mode)
  6. Paste the expression from expression.txt as a single line
  7. Set action to Block → Save

Option 2: API / Script

You need two values from your Cloudflare dashboard:

  • Zone ID — right sidebar of your zone's Overview page
  • API Token — create at dash.cloudflare.com/profile/api-tokens with Zone:Edit permission
export CF_ZONE_ID=your_zone_id_here
export CF_AUTH_TOKEN=your_api_token_here
bash block-bots.sh

Warning: The script uses PUT which replaces the entire custom WAF ruleset. If you have existing custom rules, add them to the rules array in block-bots.sh before running.

To check existing rules first:

curl "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
  -H "Authorization: Bearer $CF_AUTH_TOKEN" | jq '.result.rules'

Verifying It Works

curl -I https://yourdomain.com/.env
# HTTP/2 403

Check Security → Events in the Cloudflare dashboard — blocked requests appear there with the rule listed as the match reason.

Extending the Rule

Add more or clauses to the expression. Useful additions:

or (http.request.uri.path contains "/.ssh/")
or (http.request.uri.path contains "/etc/passwd")
or (http.request.uri.path contains "/proc/self/")
or (http.request.uri.path contains "/.DS_Store")
or (http.request.uri.path contains "/wp-content/")
or (http.request.uri.path eq "/.htaccess")

The free plan allows up to 5 custom WAF rules. The expression above is well within the length limit.

Notes

  • This is noise reduction, not a security perimeter. A targeted attacker can route around it.
  • If your app has a legitimate /backend/ route, remove that clause before deploying.
  • .env being web-accessible is a root-cause problem — fix the deployment, don't rely on WAF to hide it.
(http.request.uri.path contains "/.env") or (http.request.uri.path contains "/.git/") or (http.request.uri.path contains "/.aws/") or (http.request.uri.path contains "/actuator/") or (http.request.uri.path contains "/wp-admin/") or (http.request.uri.path contains "/wp-login") or (http.request.uri.path contains "/xmlrpc") or (http.request.uri.path contains "/phpmyadmin/") or (http.request.uri.path contains "/laravel/") or (http.request.uri.path contains "/backend/") or (http.request.uri.path eq "/config.php") or (http.request.uri.path eq "/server-status")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment