---
key: "edge-waf-bot-defense"
title: "Operate edge rules without hiding application defects"
url: "https://softwaresecurity.com/domains/runtime/edge-defense/edge-waf-bot-defense/"
api: "https://softwaresecurity.com/api/v1/articles/edge-waf-bot-defense.json"
domain: "runtime"
topic: "Runtime, cloud \u0026 APIs"
type: "Guide"
identifiers: ["A07:2025","API6:2023","API2:2023","V2.4.1","V6.3.1"]
tags: ["Denial of service and resource limits","Account takeover","Authentication and sessions","WAFs and edge defenses"]
stages: ["operate"]
roles: ["appsec-engineer","platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Operate edge rules without hiding application defects

Tune WAF and abuse controls against legitimate traffic, parser behavior and measurable business effects.

Canonical URL: https://softwaresecurity.com/domains/runtime/edge-defense/edge-waf-bot-defense/

## In short

Treat a WAF as a tuned filter, not as the application's authorization. When OWASP CRS blocks legitimate traffic, read the matched rule IDs and the target, then exclude only that rule or tag for that parameter on that route; never switch the engine off for the route. Start at paranoia level 1 with the default anomaly threshold of 5, keep the application fix that the exclusion now relies on, and handle bots by the business action they abuse.

- CRS adds a score for every matched rule and blocks when the inbound total reaches the threshold, 5 by default.
- Exclude by rule ID or tag, parameter and route: `ctl:ruleRemoveTargetByTag=attack-xss;ARGS:comment` kept SQL injection checks on that field.
- An exclusion moves the defense to the application: after it, a real `<script>` payload in that field also passed, so output encoding must hold.
- Paranoia levels 1 to 4 trade false positives for coverage; CRS 4 can detect at a higher level than it blocks while you tune.
- Bots abuse valid requests: map them to OWASP OATs such as OAT-008 credential stuffing and OAT-021 denial of inventory, and limit the business action.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| `tx.blocking_paranoia_level` | 1, raised only with a tuning period | — | OWASP CRS anomaly scoring |
| `tx.detection_paranoia_level` | Equal to the blocking level by default; set one level higher while tuning | — | OWASP CRS anomaly scoring |
| Inbound and outbound anomaly thresholds | 5 and 4 | — | OWASP CRS anomaly scoring |
| Rule exclusions | Rule ID or tag plus one target (`ARGS:name`) on one route; never `ctl:ruleEngine=Off` | — | OWASP CRS false-positive tuning |

## Verify it

1. Reproduce the false positive and read the matched rule IDs and targets from the ModSecurity audit log before writing any exclusion.
2. After the exclusion, confirm the legitimate request passes and that an attack of another class in the same field, the same text in another field and the same field on another route are still blocked.
3. Send a real exploit payload in the excluded field and confirm the application encodes or rejects it, because the WAF no longer will.
4. Run the change in detection-only or with a higher detection paranoia level first and compare anomaly scores on real traffic.
5. For each bot-facing flow, record which OAT it addresses and test the per-account limit, not only the per-address one.

## What you will learn

- Design an exclusion around one false positive without disabling unrelated inspection
- Choose limits for distributed credential stuffing that a per-address rate limit cannot see
- Distinguish pattern filtering from business authorization

Audience: Platform and security engineers who operate a WAF or CDN rule set and the teams that own the applications behind it

Before you start:

- Distinguish edge decisions, origin behavior, false positives and business abuse.

Time: About 28 minutes: 8 to read and 20 for the exercise

## An edge control sees a different view from the application

A WAF inspects traffic before the application's full business context is available. It can reduce exposure to recognized patterns and provide temporary containment, but it cannot reliably infer every valid account relationship or repair a transaction race. Treat a virtual patch as a tracked mitigation with a linked application fix and retirement condition.

RASP observes execution inside a running application, while IAST uses instrumentation during testing to find defects. These locations change available context, deployment cost and failure behavior. Evaluate each against a concrete task rather than assuming that adding another category guarantees complete protection.

## Worked exclusion: a comment field contains legitimate punctuation

The synthetic rule model below, with invented rule names, keeps the exercise small; the next section shows the same pattern with the real OWASP CRS. R-A flags an apostrophe in any body field, while R-B flags the literal test marker `BAD_TEST` in any body field. A legitimate comment, `O'Connor's booking`, trips the simplistic R-A. Disabling all inspection on the route would also disable R-B and other controls. Excluding only R-A on the comment parameter preserves the rest of the modeled inspection.

Real CRS tuning requires the actual matched rule, target variable, transformation and anomaly score. One rule can inspect many parameters, and multiple matches may contribute to a blocking score. Capture a redacted reproducible legitimate request and compare narrowly scoped exclusions under the installed ruleset. The intended lesson is how to define scope and regression cases, not to recommend removing a named production rule without evidence.

The application must still use safe database APIs and enforce ownership. A WAF cannot infer that authenticated North user N may not cancel South user S’s booking when both requests are syntactically ordinary. Keep an application regression for that relationship even if the edge blocks known injection patterns. Track a temporary virtual patch with its owner, expiry, affected route and application repair.

Bots are the other half of edge work, and signatures rarely see them. In the exercise trace, 40,000 login attempts arrive in an hour from 9,800 residential addresses, none sending more than five, each with a different leaked username and password pair. Every request is well-formed, so no CRS rule matches, and a limit of 20 requests a minute per address never fires. This is OWASP OAT-008 credential stuffing. The controls that bite key on what the attacker reuses or targets, not on the address: failed logins per account and per username, a check of submitted passwords against known breached passwords, and phishing-resistant MFA or passkeys so a correct password is not enough. The MFA and login defense guide at /domains/identity-crypto/authentication/mfa-account-takeover/ covers those controls.

```text
SYNTHETIC RULES, not production CRS identifiers
R-A: any ASCII apostrophe in any body field -> match
R-B: literal BAD_TEST in any body field -> match
Any non-excluded match blocks
Candidate exclusion: R-A only, path=/comments, parameter=comment
T1 /comments comment="O'Connor"
T2 /comments comment="BAD_TEST"
T3 /comments username="O'Connor"
T4 /profile comment="O'Connor"
```

## Write a narrow exclusion for OWASP CRS

With CRS 4.30 running on ModSecurity at paranoia level 1, a developer-forum comment, `wrap it in <script> tags`, posted to `/comments` was blocked with 403. The audit log named three matches on `ARGS:comment` (rules 941100, 941110 and 941160, all tagged `attack-xss`) for an anomaly score above the threshold of 5, which rule 949110 enforced. The exclusion below removes the XSS rules from that one parameter on that one route.

Tested with the same requests as the exercise: the legitimate comment passed; a SQL injection string in the same field, the same HTML in a `username` field and the same comment posted to `/profile` were all still blocked. A route-wide `ctl:ruleEngine=Off` also let the legitimate comment through, but it let the SQL injection and the `username` case through as well. Even the narrow exclusion has a cost: `<script>alert(1)</script>` in the comment field now passed the WAF, so the application must encode comments on output, and that test belongs in its regression suite.

```apache
# Developers post HTML snippets in the comment field of POST /comments. Stop the XSS rules
# from inspecting that one parameter on that one route; every other rule still applies to it,
# and every rule still applies to other parameters and routes.
SecRule REQUEST_FILENAME "@streq /comments" \
    "id:10000,\
    phase:1,\
    pass,\
    nolog,\
    ctl:ruleRemoveTargetByTag=attack-xss;ARGS:comment"
```

## Tune narrow exceptions with representative traffic

Start with a documented ruleset and a controlled observation phase. CRS groups rules into paranoia levels 1 to 4: level 1 aims for few false positives, and each higher level adds stricter rules that catch more attacks and more legitimate traffic. CRS 4 separates `tx.blocking_paranoia_level` from `tx.detection_paranoia_level`, so rules from a higher level can score and log without blocking while you tune. By default it blocks a request whose inbound anomaly score reaches 5 and a response whose score reaches 4. OWASP CRS uses rule matches and anomaly scoring; review which inputs triggered which rules instead of disabling a broad protection because a legitimate request was blocked. Keep a fixture for the legitimate request and one for the intended rejection when introducing an exclusion.

Limit exclusions to the necessary endpoint, parameter and rule where the implementation supports that scope. Verify limits and body inspection behavior for uploads, compressed data and large payloads. Understand what the edge does when inspection fails or a backend is unavailable. Monitor false-positive impact on real tasks, not only the percentage of traffic blocked.

## Keep intermediaries consistent about the request and response

Request smuggling arises when components disagree about request boundaries or framing; the guide at /domains/runtime/edge-defense/http-request-desynchronization/ shows how to test a chain. Use maintained proxy/server implementations, reject ambiguous framing and verify HTTP-version translation. Parser disagreement can undermine routing and WAF assumptions even when the visible request looks ordinary. Run such checks only in an isolated environment because shared connections and caches can affect other traffic.

Shared caches at the edge are a separate boundary: a cache that stores a personalized response under an incomplete key leaks it to the next user without any malicious request. The cache poisoning and deception guide at /domains/runtime/edge-defense/cache-poisoning-deception/ covers cache keys, private responses and how to test them; no WAF rule or threshold repairs a cache key.

## Rate-limit the business consequence as well as traffic

Credential stuffing, scraping and inventory hoarding may use syntactically valid authenticated requests. Combine account/tenant/action limits, risk signals and suitable authentication controls with upstream capacity protection. Avoid permanent account lockouts that let an attacker deny access to a victim. Challenges need accessible alternatives and measured failure rates. Test recovery after the abusive pattern stops and keep application permission checks independent of the edge decision.

The OWASP Automated Threats to Web Applications project names 21 kinds of abuse, OAT-001 to OAT-021, which gives teams a shared vocabulary for which flow is attacked and which control applies. Most need application or identity controls rather than WAF signatures.

| OWASP OAT | Abused flow | Controls that limit it |
| --- | --- | --- |
| OAT-008 Credential Stuffing | Login with leaked username and password pairs | Phishing-resistant MFA or passkeys, breached-password checks, per-account and per-network limits |
| OAT-007 Credential Cracking | Guessing passwords for known accounts | Per-account throttling with backoff instead of permanent lockout |
| OAT-019 Account Creation | Bulk sign-ups for spam or bonuses | Verification of a contact channel, per-network limits, delayed privileges |
| OAT-011 Scraping | Bulk collection of pages or API data | Authenticated access, per-account quotas, pagination limits |
| OAT-021 Denial of Inventory | Holding stock or seats without buying | Per-account hold limits and short hold expiry |
| OAT-005 Scalping | Buying limited goods faster than people can | Per-account purchase limits, queueing, verified buyers |

## Diagnose incomplete fixes

A request that exceeds the inspection body limit may be rejected, partially inspected or passed according to the product and configuration. Test that explicit failure mode. A dashboard showing inspected requests tells you little about traffic skipped by the inspection path.

Bot controls also need a defined unit of abuse. Account recovery, reservation holds and expensive search may require distinct per-account, tenant and action budgets. A challenge or IP limit that blocks a shared office network can harm legitimate users while leaving distributed abuse intact. Measure successful user tasks and recovery after pressure stops.

## Check the result against concrete cases

These outcomes belong to the synthetic model; the CRS test above produced the same pattern for T1 to T4 with real rules.

| Case | Expected result | What it establishes |
| --- | --- | --- |
| T1 with narrow exclusion | Pass these modeled rules | Legitimate comment restored |
| T2 | Block R-B | Other rule on same parameter retained |
| T3 | Block R-A | Other parameter remains inspected |
| T4 | Block R-A | Other endpoint remains inspected |
| Ordinary cross-owner cancellation | Application denies ownership | Business authorization remains independent |
| 40,000 logins from 9,800 addresses, five or fewer each | Per-account and breached-password controls engage; the per-address limit alone never fires | Bot controls key on the abused account, not only the address |

## Guided practice

Evaluate the four rule fixtures exactly under the synthetic model, then decide how to stop the login abuse described below.

### Supplied material

```text
Exclusion E1: disable every rule for /comments.
Exclusion E2: disable R-A only for /comments parameter comment.
B1 POST /login: 40,000 attempts in one hour; 9,800 distinct client addresses; at most 5 per address.
B2 Each attempt uses a different username and password pair; 61% of usernames exist; 0.4% of attempts succeed.
B3 Current controls: WAF with CRS at paranoia level 1; rate limit 20 requests per minute per address; no MFA.
A normal authenticated request by N attempts to cancel a booking owned by S.
```

### 1. Compare exclusions

Apply E1 and E2 to T1–T4 and explain which preserves the intended test rejection.

Worked answer: E1 lets T1, T2 and T3 through on `/comments` and still blocks T4. E2 lets only T1 through; T2, T3 and T4 remain blocked. E2 preserves the modeled `BAD_TEST` rejection and other parameter/route checks.

### 2. Choose the bot controls

Use B1 to B3 to explain why the current controls miss the attack and which controls would limit it.

Worked answer: Every request is syntactically normal, so CRS has nothing to match, and no address exceeds the per-address limit. This is credential stuffing (OAT-008). Add per-account and per-username failure limits with backoff rather than permanent lockout, check passwords against breached-password lists, require phishing-resistant MFA or passkeys, and treat the accounts with successful logins as compromised: reset their sessions and credentials.

### 3. Keep the application test

Decide whether passing edge inspection authorizes N’s cancellation.

Worked answer: No. The application must check the booking owner or delegated permission and deny N under the stated ownership model. Pattern inspection has no supplied authority to grant that business action.

### 4. Define release evidence

Specify positive and negative checks for the final repair.

Worked answer: Retain T1 as a legitimate successful request and T2 to T4 as rejection controls under the model. Replay a distributed login test from many addresses and confirm the per-account limit and MFA stop it while a real user still signs in. Verify an owner can cancel and a non-owner cannot.

Deliverable: The T1 to T4 results under E1 and E2, the bot controls with the key each one limits on, the application ownership decision and the release checks.

Scope: The rule evaluation uses the synthetic model; the CRS exclusion on this page shows the same scoping with real rules.

## Executable practice

- [Repair a real proxy and shared cache: project ZIP](https://softwaresecurity.com/downloads/projects/proxy-cache-boundaries-66af6733aa29700d.zip) — Node.js 22+ · loopback servers · no dependencies

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OWASP CRS anomaly scoring](https://coreruleset.org/docs/2-how-crs-works/2-1-anomaly_scoring/)
- [OWASP CRS false-positive tuning](https://coreruleset.org/docs/2-how-crs-works/2-3-false-positives-and-tuning/)
- [RFC 9112 HTTP framing](https://www.rfc-editor.org/rfc/rfc9112.html)
- [RFC 9111 caching](https://www.rfc-editor.org/rfc/rfc9111.html)
- [OWASP Credential Stuffing Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html)
- [OWASP CRS: paranoia levels](https://coreruleset.org/docs/2-how-crs-works/2-2-paranoia_levels/)
- [OWASP CRS Docker images](https://github.com/coreruleset/modsecurity-crs-docker)
- [OWASP Automated Threats to Web Applications](https://owasp.org/projects/automated-threats-to-web-applications)