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.
| 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.
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.
(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
- Log into Cloudflare → select your zone
- Go to Security → WAF → Custom rules
- Click Create rule
- Name it anything ("Block scanner bots")
- Switch expression editor to Edit expression (text mode)
- Paste the expression from
expression.txtas a single line - Set action to Block → Save
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-tokenswithZone:Editpermission
export CF_ZONE_ID=your_zone_id_here
export CF_AUTH_TOKEN=your_api_token_here
bash block-bots.shWarning: The script uses
PUTwhich replaces the entire custom WAF ruleset. If you have existing custom rules, add them to therulesarray inblock-bots.shbefore 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'curl -I https://yourdomain.com/.env
# HTTP/2 403Check Security → Events in the Cloudflare dashboard — blocked requests appear there with the rule listed as the match reason.
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.
- 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. .envbeing web-accessible is a root-cause problem — fix the deployment, don't rely on WAF to hide it.
