A WAF rule is just a condition plus an action, evaluated against every request that hits your site: if this part of the request looks like that, do this. The concept is simple. The syntax, historically, has not been — ModSecurity's SecLang (which Coraza, the engine ShieldIngress runs, also speaks) has its own quoting rules, its own escaping requirements, and a habit of failing silently or in confusing ways when a rule ID collides with another rule or a variable name is slightly wrong.

Our WAF Rule Builder exists to remove that entirely: you pick fields from dropdowns, and it generates a rule that's syntactically correct by construction. This walks through what each field actually does, so the rule you generate does what you actually intended, not just what compiles.

Block vs. Allow/Exempt#

The first choice is what kind of rule you're building, and they solve two different problems:

  • Block matching requests — a brand new rule: traffic matching your conditions gets blocked (or logged, depending on the Response field below). Use this when you've spotted a specific pattern you want to stop that your existing ruleset isn't already catching.
  • Allow / exempt matching requests from a WAF rule — for the opposite, more common problem: your existing managed ruleset (ShieldIngress runs the OWASP Core Rule Set by default) fires a false positive against something legitimate. Rather than writing a new rule, this generates an exemption for one specific existing rule ID.

The exemption path has a second, important choice: disable that rule entirely for matching requests, versus only skip one field within it. The difference matters a lot in practice. Say a rich-text editor's save endpoint legitimately submits HTML-looking content in a field called content, and that trips an XSS detection rule. Disabling the rule entirely means that request gets zero XSS inspection at all, everywhere. Scoping the exemption to just ARGS:content on that one field means the rule keeps inspecting everything else about the request exactly as before — only that one legitimate field is excluded. Prefer the narrower option whenever you can; it's exactly as much protection as you had, minus only the one specific false positive.

Match conditions: variables#

Each condition starts with picking what part of the request to inspect:

VariableWhat it inspects
REQUEST_URIThe path being requested, e.g. /login or /api/users/42
QUERY_STRINGEverything after the ? in the URL, as one raw string
ARGSAny request parameter — query string or POST body — checked individually
ARGS_NAMESThe names of parameters, not their values (useful for catching an unexpected parameter appearing at all)
REQUEST_HEADERSRequest header values
REQUEST_HEADERS_NAMESHeader names present on the request
REQUEST_COOKIESCookie values sent by the client
REQUEST_BODYThe raw request body, unparsed
REQUEST_LINEThe full first line of the request (method + path + protocol)
REQUEST_METHODGET, POST, PUT, DELETE, etc.
FILES_NAMESFilenames of any uploaded files
REMOTE_ADDRThe client's IP address

For ARGS, REQUEST_HEADERS, and REQUEST_COOKIES, a second field appears: a subkey. Leave it blank and the condition checks any parameter/header/cookie. Fill it in — say, user_id — and it narrows the check to just that one, e.g. ARGS:user_id instead of every argument on the request. Narrower is almost always better: it avoids a rule accidentally matching an unrelated field that happens to contain similar-looking text.

Match conditions: operators#

OperatorMatches when
@containsThe value contains this substring anywhere
@streqThe value equals this exactly
@beginsWith / @endsWithThe value starts or ends with this
@rxThe value matches this regular expression
@pmThe value matches any phrase in a comma-separated list — a fast way to check against several fixed strings at once
@ipMatchThe value is a specific IP address or falls inside a CIDR range
@detectSQLi / @detectXSSRuns a built-in classifier that recognizes SQL injection or XSS patterns generally, rather than matching one specific string
Be careful with @rx. Regular expressions are the most powerful operator here and also the easiest to get subtly wrong. The tool handles escaping the pattern correctly into valid SecLang syntax for you, but the regex itself is still your responsibility — a pattern with nested repetition (like (a+)+) can exhibit catastrophic backtracking against a crafted input, effectively becoming a denial-of-service vector against your own WAF. When a simpler operator (@contains, @beginsWith) covers what you need, prefer it over a regex.

Negate and case-insensitive#

Every condition also has two toggles. Negate flips the condition to match when it's not true — for example, blocking a request where a required header is absent, rather than matching when something specific is present. Case-insensitive matters more than it sounds: HTTP header names and values, and a lot of real attack payloads, get deliberately varied in case specifically to slip past a case-sensitive rule (SeLeCt instead of select, as one classic example). Leave case-insensitivity on unless you have a specific reason a match should be case-exact.

Combining conditions#

Adding more than one condition combines them with AND — every condition must match for the rule to fire. This is how you scope a rule tightly: for example, a condition on REQUEST_METHOD @streq POST combined with a condition on REQUEST_URI @beginsWith /api/upload only evaluates the rest of the rule against POST requests to that one path, leaving every other request on your site untouched by it.

Response: Block vs. Detect#

Block (HTTP 403) actually stops the matching request. Detect, log only evaluates the same condition and logs a match, without blocking anything.

Start every new custom rule in Detect mode. This is exactly the same "log first, enforce second" approach we recommend for a new rate limit: deploy the rule in log-only, watch your WAF Events for a few days to see what it actually matches in real traffic, confirm there are no false positives against legitimate visitors, and only then switch it to Block. A rule that looked obviously correct on paper can still catch something you didn't anticipate once it sees real traffic.

Rule ID#

Every SecRule needs a numeric ID, and it must be unique across your entire ruleset — a collision with another rule (including one already in the managed OWASP Core Rule Set) causes the engine to reject the whole rule outright, not just the duplicate. A practical convention: pick your custom rules a high, clearly-out-of-range ID block — for example, anything above 1,000,000 — so there's no realistic chance of ever colliding with CRS's own numbering scheme, which stays in well-documented, much lower ranges.

Description#

Optional, but worth filling in every time. It becomes the rule's msg field, which is what actually shows up in your WAF Events log when the rule fires. "Blocks direct access to backup files" is far more useful at 2am than a bare rule ID with no context.

Common mistakes, at a glance#

SymptomLikely cause
Rule doesn't take effect at allRule ID collides with an existing rule (often a CRS rule you didn't realize was already using that number)
Rule fires on unrelated requestsVariable is too broad (bare ARGS instead of a specific subkey), or missing a second AND condition to narrow scope
Rule doesn't fire when it shouldCase-insensitive is off and the attacker varied casing, or the wrong operator was picked (@streq instead of @contains for a partial match)
Exemption disabled more than intendedUsed "disable entirely" instead of the narrower "only skip one field" scope

Paste it straight into your dashboard

ShieldIngress runs the OWASP Core Rule Set by default, so most tenants only ever need an exemption, not a brand-new rule. When you do need one, the exact SecRule syntax this tool generates is what our Advanced Rules feature accepts directly.