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:
| Variable | What it inspects |
|---|---|
| REQUEST_URI | The path being requested, e.g. /login or /api/users/42 |
| QUERY_STRING | Everything after the ? in the URL, as one raw string |
| ARGS | Any request parameter — query string or POST body — checked individually |
| ARGS_NAMES | The names of parameters, not their values (useful for catching an unexpected parameter appearing at all) |
| REQUEST_HEADERS | Request header values |
| REQUEST_HEADERS_NAMES | Header names present on the request |
| REQUEST_COOKIES | Cookie values sent by the client |
| REQUEST_BODY | The raw request body, unparsed |
| REQUEST_LINE | The full first line of the request (method + path + protocol) |
| REQUEST_METHOD | GET, POST, PUT, DELETE, etc. |
| FILES_NAMES | Filenames of any uploaded files |
| REMOTE_ADDR | The 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#
| Operator | Matches when |
|---|---|
| @contains | The value contains this substring anywhere |
| @streq | The value equals this exactly |
| @beginsWith / @endsWith | The value starts or ends with this |
| @rx | The value matches this regular expression |
| @pm | The value matches any phrase in a comma-separated list — a fast way to check against several fixed strings at once |
| @ipMatch | The value is a specific IP address or falls inside a CIDR range |
| @detectSQLi / @detectXSS | Runs a built-in classifier that recognizes SQL injection or XSS patterns generally, rather than matching one specific string |
(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.
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#
| Symptom | Likely cause |
|---|---|
| Rule doesn't take effect at all | Rule ID collides with an existing rule (often a CRS rule you didn't realize was already using that number) |
| Rule fires on unrelated requests | Variable 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 should | Case-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 intended | Used "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.