Skip to content

WAF Custom Rules -- Write Your Own Rules

DodaTech 8 min read

In this tutorial, you'll learn about WAF Custom Rules. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Cloudflare WAF custom rules allow you to write your own security logic using Cloudflare's expression language, combining request fields (IP, country, URI, headers, user agent) with operators and actions to block, challenge, or allow traffic. Custom rules run after managed rulesets and give you precise control over edge-level security without modifying application code.

Why Custom Rules Matter

Managed rulesets protect against known attack patterns, but every application has unique security requirements. You may need to block traffic from specific countries, protect administrative paths from non-office IPs, or allowlist internal monitoring tools that would otherwise trigger false positives. Custom rules handle these scenarios. They also let you implement business logic at the edge -- for example, blocking requests without a specific header, enforcing API Versioning, or rate-limiting unauthenticated access to search endpoints.

Real-World Use Case

A B2B SaaS platform experienced credential stuffing attacks targeting their customer login endpoint. The attacks came from a rotating set of residential proxies in three countries where they had no customers. They wrote a custom WAF rule that blocked POST requests to /api/login from those countries, with an expression combining ip.geoip.country and http.request.uri.path. The rule blocked 98% of credential stuffing attempts immediately, with zero impact on legitimate users.

Custom Rule Evaluation Order

flowchart TD
    R[Incoming Request] --> M[Managed Rulesets]
    M --> C[Custom Rules - First Match Wins]
    C -->|Allow / Skip| A[Pass Through]
    C -->|Block| B[Drop Request]
    C -->|Challenge| J[JS Challenge / CAPTCHA]
    C -->|Log| L[Record Only]
    
    subgraph "Expression Components"
        F1[ip.src]
        F2[ip.geoip.country]
        F3[http.request.uri.path]
        F4[http.user_agent]
        F5[http.request.method]
        F6[cf.bot_management.score]
    end
    
    style C fill:#e67e22,color:#fff
    style B fill:#e74c3c,color:#fff

Custom rules are evaluated in order, and the first matching rule determines the action. Rules can be reordered in the dashboard or by specifying a priority field. An allow or skip rule prevents subsequent rules from evaluating, while a block or challenge rule terminates the request.

Expression Language Basics

Cloudflare WAF expressions use a simple field-operator-value syntax:

field operator value
  • Fields: ip.src, ip.geoip.country, http.request.uri.path, http.request.method, http.user_agent, http.host, cf.bot_management.score, ssl
  • Operators: eq, ne, contains, starts_with, ends_with, in, not in, lt, gt, le, ge
  • Values: strings, IP addresses, CIDR ranges, numbers, arrays
  • Combinators: and, or, not, parentheses for grouping

Basic Custom Rule Examples

# Example 1: Block requests from specific countries
# Using the Cloudflare API to create a custom rule
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/phases/http_request_firewall_custom/rules" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Block traffic from high-risk countries",
    "expression": "(ip.geoip.country in {\"T1\" \"RU\" \"CN\" \"KP\" \"IR\"})",
    "action": "block",
    "enabled": true
  }' | python3 -m json.tool
# Expected output:
# {
#   "result": {
#     "id": "custom-rule-1",
#     "description": "Block traffic from high-risk countries",
#     "action": "block",
#     "enabled": true
#   },
#   "success": true
# }

This rule blocks requests from specified countries. Note the space-separated array syntax with curly braces. You can find country codes using the ISO 3166-1 alpha-2 standard. The rule is evaluated for every request to your zone.

Protecting Admin Paths

# Example 2: Allow only office IPs to access admin paths
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/phases/http_request_firewall_custom/rules" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Admin access restricted to office IPs",
    "expression": "(http.request.uri.path starts_with \"/admin\" and ip.src ne 203.0.113.0/24)",
    "action": "block",
    "enabled": true
  }' | python3 -m json.tool
# Expected output:
# {
#   "result": {
#     "id": "custom-rule-2",
#     "description": "Admin access restricted to office IPs",
#     "action": "block",
#     "enabled": true
#   }
# }

This rule blocks requests to /admin paths unless they come from the office IP range. The starts_with operator matches /admin, /admin/dashboard, and any other path beginning with /admin. Use more specific paths if you need finer control.

Bot Score-Based Blocking

# Example 3: Block requests with low bot score
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/phases/http_request_firewall_custom/rules" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Block automated traffic on login endpoint",
    "expression": "(http.request.uri.path eq \"/login\" and cf.bot_management.score lt 30 and http.request.method eq \"POST\")",
    "action": "challenge",
    "enabled": true
  }' | python3 -m json.tool
# Expected output:
# {
#   "result": {
#     "id": "custom-rule-3",
#     "description": "Block automated traffic on login endpoint",
#     "action": "challenge",
#     "enabled": true
#   }
# }

Bot scores range from 1 (definitely automated) to 99 (definitely human). This rule challenges (presents a CAPTCHA) POST requests to /login with a bot score below 30. This blocks credential stuffing and brute force attacks while allowing legitimate users through after completing the challenge.

Combining Multiple Conditions

# Example 4: Block suspicious user agents on API endpoints
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/phases/http_request_firewall_custom/rules" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Block suspicious user agents on API",
    "expression": "(http.request.uri.path starts_with \"/api/v2\" and http.user_agent in {\"curl\" \"python-requests\" \"Go-http-client\"} and http.request.method ne \"GET\")",
    "action": "block",
    "enabled": true
  }' | python3 -m json.tool
# Expected output:
# {
#   "result": {
#     "id": "custom-rule-4",
#     "description": "Block suspicious user agents on API",
#     "action": "block",
#     "enabled": true
#   }
# }

This rule blocks non-GET requests to the API from common scripting tools. It allows legitimate GET requests (used by monitoring and health checks) while blocking automated POST, PUT, and DELETE attempts from these user agents.

Listing and Managing Custom Rules

# List all custom rules in the zone
curl -s -X GET "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/phases/http_request_firewall_custom/entries" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for rule in data['result']:
    print(f'ID: {rule[\"id\"]}, Action: {rule[\"action\"]}, Desc: {rule.get(\"description\", \"N/A\")}, Enabled: {rule.get(\"enabled\", True)}')
"
# Expected output:
# ID: custom-rule-1, Action: block, Desc: Block traffic from high-risk countries, Enabled: True
# ID: custom-rule-2, Action: block, Desc: Admin access restricted to office IPs, Enabled: True

List all custom rules with their IDs, actions, descriptions, and enabled status. Use the rule IDs to update or delete specific rules. Custom rules are evaluated in their listed order -- the first matching rule wins.

Common Errors and Troubleshooting

Expression Syntax Error

If your expression has mismatched parentheses, incorrect field names, or invalid operator usage, the API returns a validation error. Solution: use Cloudflare's expression Builder in the dashboard to validate complex expressions before deploying via API. Test simple expressions first and build up.

Rules Reordering Issues

When you have multiple custom rules, the order matters. If an allow rule appears before a block rule for the same traffic, the allow wins. Solution: assign explicit priorities (1, 2, 3...) to your rules. Lower numbers are evaluated first.

Country Code Format

Country codes must be uppercase ISO 3166-1 alpha-2 format. Using lowercase codes or non-standard abbreviations causes the rule to never match. Solution: verify country codes using https://api.<a href="/web-servers-hosting/cloudflare/">Cloudflare</a>.com/client/v4/ips or ISO documentation.

Bot Score Not Available

Bot score fields require the Cloudflare Bot Management add-on. On plans without Bot Management, the cf.bot_management.score field is not populated, and rules using it never trigger. Solution: use cf.client.bot (available on all plans) for basic bot detection.

Too Many Rules

Cloudflare enforces a maximum number of custom rules per zone. The limit varies by plan. Solution: consolidate rules where possible by combining expressions with or. Delete unused rules to stay within the limit.

Practice Questions

  1. What expression field and operator would you use to block requests to any path starting with /wp-admin?
  2. How do you specify multiple country codes in a single WAF expression?
  3. What is the priority behavior when two custom rules match the same request?

FAQ

Can custom rules be tested before deploying to production?

Yes. Create a rule with the log action first. This allows you to see how many requests would match the rule and what action would be applied, without actually blocking any traffic. Review the logs in Cloudflare analytics for 24-48 hours, then change the action to block or challenge once you are confident.

What is the difference between a custom rule and a rate limiting rule?

Custom rules operate on individual requests -- they evaluate each request independently and apply a static action (allow, block, challenge, log). Rate Limiting rules track request frequency from each client and apply an action only after a threshold is exceeded. Rate Limiting uses a different API and has its own expression syntax with requests_per_period and period fields.

Can custom rules use regex patterns?

Cloudflare WAF expression language does not support regex directly in custom rules. Use operators like contains, starts_with, ends_with, and in for pattern matching. For regex-level matching, use Cloudflare Workers or Transform Rules with regex support before the request reaches the WAF phase.

Summary

Cloudflare WAF custom rules give you fine-grained control over edge-level security using a simple expression language. You can combine fields like IP, country, URI, user agent, and bot score with operators to create rules that match your application's specific security requirements. Rules support four actions (allow, block, challenge, log) and are evaluated in priority order. Test custom rules with the log action before enabling blocking, and regularly review rule effectiveness through Cloudflare analytics.

This guide is brought to you by the developers of Cloudflare, Web Security, and DodaZIP at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro