Oink, engineering folks! Pigman at the keyboard. Today we're looking at a guard who protects the shop so diligently that the customer can't update their cart. The chat's down too. At least the 403 response works flawlessly. Meet false positives in a WAF, illustrated with OWASP Coraza and Juice Shop.

Let me get my hooves straight right away: this test setup was built by the author of the lab walkthrough, not me. They used Coraza with OWASP CRS, Caddy, and the intentionally vulnerable Juice Shop. I'll take you through five triggers: where the policy and the application's logic diverged, how the exceptions were narrowed, and why a working cart doesn't mean the site is secure.

The customer came for a product. They got suspicion instead

The experiment plan was modest: a Python script with Playwright logs into the shop, searches for a product, adds it to the cart, and proceeds to checkout. With no custom exceptions configured, the WAF stopped some of these actions. We're still a long way from payment, and the adventures have already begun.

The tricky thing about an SPA is that you can see a perfectly respectable interface. The page rendered, the buttons are in place, the designer can sleep soundly. But a background request got 403 — and the action you need doesn't happen. So I'd look at the entire flow. The fact that “the home page opened” proves only that the home page opened.

CRS looks for signs of suspicious requests, but it can't read the mind of the developer who built your shop. For an API, the PUT method may be a normal data update. For the configured policy, it may be a method outside the allowed list. Both sides are doing their jobs. An engineer will have to explain one to the other.

949110 is the verdict. Pull up the preceding lines

We open audit.log. At the end of the chain, you'll often find 949110, or Inbound Anomaly Score Exceeded. Is your hand already reaching to switch off the culprit? Hold your hoof. This rule reports the accumulated score; the specific reasons may have been detected by other checks above.

In the shown configuration with Paranoia Level 1, the threshold was 5 points. A CRITICAL trigger added 5, WARNING — 3. The points add up: remove one reason but leave the others, and the request can still score enough to be blocked. The arithmetic is relentless, even when you really want to close the ticket.

The author looked at id, the message msg and is_interrupted: true. What matters to me here is the whole route: which signal triggered, how many points it added, and what ultimately interrupted the request. That's how we look for the cause of a failure, instead of just teaching the log to shut up.

Five suspects in the case of the missing cart

First — Socket.IO with text/plain. The rule 920420 rejected the content type of the service's HTTP requests. In the test setup, they came in with Content-Type: text/plain. We're talking about a handshake or polling, not inspecting frames in an already established WebSocket connection. I'll deliberately slow down here: if you mix these things up, you'll explain the failure at an entirely different layer.

Second — a cart that needed PUT. The request to /api/BasketItems/1 ran into the rule 911100. The configuration allowed GET, POST, HEAD, and OPTIONS. PUT wasn't invited to the club. The application asks to update data, the policy refuses — and the customer is left with unfulfilled consumer plans.

Third — a profile named whoami. The rule 932260 recognized a word in /rest/user/whoami that looks like a Unix command. But here it's the name of a REST endpoint. Nobody's running a command. The check found a familiar signal, but the application's context didn't come with it.

Fourth — the search for apple'--. The apostrophe and two hyphens caught the attention of 942100 from LibInjection. In the lab scenario, this was a test search value. It looked suspicious to the SQLi detector. This is where it gets really interesting, piggy: making the search work is easy. Preserving the check you need while doing it is a separate job.

Fifth — an IP address in Host. The rule 920350 added 3 points for a numeric Host. By itself, it wasn't enough to block the request in this example, but it could add up with other warnings. So I wouldn't pin the blame on one suspect just because their name was the last to catch our eye.

We fix the cart. We don't tear down the whole fence

The author gathered the exceptions in coraza-rules/exclusions.conf. Instead of a global SecRuleRemoveById they used a targeted target exclusion via ctl:ruleRemoveTargetById. The rule continued to apply to the rest of the requests. The boundaries of an exception still need to be checked: the word “targeted” doesn't automatically grant a security certificate to the setting.

The first attempt at fixing the search was too broad. The parameter q for /rest/products/search was excluded from the 942100check. The false positive disappeared, but that check no longer analyzed the excluded parameter. My favorite kind of green test: stop checking, and it stops complaining. Only that's not a fix yet.

Then the author added a condition via chain: the exception applies if the value doesn't contain specified SQL patterns, including union, select or or 1=1. Here's where I'll put a big editor's snout: the source explicitly calls this a simplified educational example. That doesn't make it a ready-to-use production rule.

Obfuscation, encoding, and SQL comments can bypass a regular expression like this. The walkthrough separately mentions normalization and t:urlDecodeUni, t:lowercasetransformations. So I don't recommend dropping this regex into your own config without understanding it. You'll need to check both the exception's boundaries and which requests it now lets through.

The customer got through. Now it's time for the uninvited guests

After the changes, Playwright made it to the checkout button. The log no longer showed blocks for this scenario under the five rules we examined. Good: functionality was restored in the test setup. But I wouldn't put down the keyboard just yet or pour celebratory coffee.

The author separately sent test requests with SQLi, RCE, XSS, Path Traversal, and forbidden DELETE method signatures. They continued to receive 403. That's the result of specific checks in a specific test setup, not a promise to block any attack after changes like these.

The test setup used Coraza with CRS 4.25.2 and Caddy. For production, the author recommends versioning exceptions, reviews, monitoring, and automated regression testing; ideally, narrow the exception's scope to the method, URL, and parameter. That's Pigman's kind of ending: the cart works, test attacks are blocked, and the next engineer can understand the config without summoning the spirits of closed tickets.