About six years ago I pushed a custom WAF rule that was meant to block SQL injection probes on a checkout API. It worked perfectly in staging, but an hour after it went live in enforce mode, our support channel lit up — every customer with an apostrophe in their last name, like O'Brien, was getting a 403. We’d written a regex that was technically correct but context-blind, and we hadn’t measured its blast radius. That night taught me that writing detection rules isn’t about catching attackers, it’s about not catching everyone else.
Why Most Custom Rules Die in the First 48 Hours
When teams start with WAF Managed Ruleset Comparison: OWASP CRS, Commercial, and Provider-Specific Rules, they often assume custom rules are just smaller versions of managed ones. They aren’t. Managed rulesets have thousands of hours of tuning and paranoia baked in; your custom rule has you, a Jira ticket, and maybe 30 minutes before the next deployment. The most common failure I’ve seen isn’t a missed attack, it’s a rule that’s far too greedy. Someone writes `(?i)(union.*select|or\s+1=1)` and applies it to `REQUEST_URI`, `ARGS`, `BODY`, and `HEADERS` all at once, without considering where that payload actually matters.
That kind of broad matching is what kills you. I’ve reviewed rules that flagged marketing UTM parameters, base64-encoded images, and even legitimate JSON fields because the author didn’t anchor the inspection to a specific location. If you’re trying to protect a login endpoint, you don’t need to inspect every single query string on your static asset CDN. You need to ask: where does this attack have to appear to be dangerous, and where would a normal user never put it? The more precisely you can answer that, the fewer false positives you’ll create. We’ve learned to start every rule draft by documenting the attack vector’s required context — method, path, content-type, and parameter name — before we write a single line of regex.
Understanding how your platform actually evaluates rules helps you write tighter logic. Not all engines normalize, decode, or parse the same way. A rule that looks for `../` won’t fire if the engine already normalized the path, and a rule that looks for `