# Scoring and modes

A web application firewall decides on accumulated evidence rather than on one suspicious pattern. Each attack pattern a request matches adds to a score, and a mode decides what happens once a score reaches its threshold. For where WAF acts in a firewall's request path, refer to [How Firewall works](/en/documentation/platform/firewall/how-it-works/#waf).

The sections cover scoring and sensitivity, modes, request body parsing, exceptions, tuning, and where a match is reported.

---

## Scoring and sensitivity

WAF does not decide on one rule match. Each internal rule that matches a request adds to the score of the threat family that rule belongs to, so one weak signal scores low and several score high. A request is blocked when a family's score reaches that family's threshold.

The scores are kept per family, and each one is compared with its own threshold. For example, the query string `1' OR '1'='1` can match internal rules of two families on the same argument, and it then carries one score for SQL injection and another for cross-site scripting.

You never write a threshold as a number. You choose a sensitivity level for each family, and the level fixes the score at which that family blocks. Every family starts at `medium`, and the scale runs against the word: a higher sensitivity is a lower threshold. At `highest` a score of 4 blocks, so less evidence refuses more requests, while at `lowest` a request needs a score of 40. For the five levels and their scores, refer to [WAF rule sets](/en/documentation/platform/firewall/waf/rules-set/#sensitivity-levels).

Sensitivity cuts both ways. Raising it catches attacks that carry less evidence. It also blocks legitimate requests that carry a little of the same evidence, such as an apostrophe in a search term. Lowering it keeps that traffic moving and lets through any attack whose score stays under the threshold. The level is set per family, so you can raise the families your application has no reason to resemble and leave the others.

---

## Modes

The mode belongs to the rule's *Set WAF* behavior, not to the rule set. In *Logging*, a request that reaches a threshold is scored and recorded, and the application receives it as though nothing had matched. In *Blocking*, that request is refused before it reaches the application. The API takes the mode as `logging` or `blocking`, and it is required: a *Set WAF* behavior without one is refused with `10059`.

A blocked request receives `400` and Azion's default error page, headed **Bad Request**. Nothing in the response names WAF, the rule, or the score: no header carries them, and the page has no message of its own. The page's Status Code row reads `0`, the status of an origin the request never reached.

Each mode trades one risk for another. *Logging* records everything that would have been blocked without refusing a legitimate request, and it protects nothing while it runs. *Blocking* protects, and every false positive becomes a `400` that a user sees, with nothing in it to explain why.

One internal rule is not fully held back by the mode. Rule 13, invalid POST format, blocks some requests even when the rule set runs in *Logging*. For what each internal rule matches, refer to [WAF rule sets](/en/documentation/platform/firewall/waf/rules-set/#internal-rules).

Other Azion surfaces call the *Logging* mode learning. [Real-Time Events](/en/documentation/platform/real-time-events/) reports it in the `wafLearning` field, the [Data Stream](/en/documentation/platform/data-stream/) payload carries `waf_learning`, and `azion.config.js` sets it as `wafMode`. The API itself refuses `learning` as a mode, with `10039`.

---

## Request body parsing

WAF reads a request body only in the formats it can take apart. On a `POST`, it parses the body when `Content-Type` is `application/x-www-form-urlencoded`, `multipart/form-data`, `application/json`, `application/vnd.api+json`, or `application/csp-report`. Parsing lets a rule address one field of the body, and it lets an exception name one field to leave alone.

A body in any other format does not pass unexamined. An unknown or missing `Content-Type` on a `POST` is a finding in its own right for the protocol-compliance rules. A rule set can therefore refuse that request with the same `400`, even when its body is benign. Internal rule `11` makes that refusal, recorded as `wafMatch` `0:11:BODY:-` with `wafAttackFamily` `$OTHERS`. Rule `15` refuses an `application/json` body that does not parse, such as `AAAA`, so the client receives a `400` from the firewall rather than a parse error from the application.

Parsing also stops at a size. A body larger than 131,072 bytes, which is 128 KiB, is refused rather than passed through uninspected. For that bound, refer to [Firewall limits](/en/documentation/platform/firewall/limits/#waf).

Reading only these formats keeps the inspection precise on what applications post. It costs coverage of the rest. A format WAF does not parse is never searched field by field, and the rule that catches it reads the `Content-Type` rather than the content.

---

## Exceptions

Some applications legitimately send what an internal rule was written to catch. A search box accepts an apostrophe, a field carries a pipe character, or an API posts a fragment of markup. Each one reaches a threshold with nothing wrong. An exception takes one such case out of scoring without lowering the sensitivity for every other request.

An exception names a part of the request and the internal rule to exempt it from. The part is a condition on a match zone, such as a query string value or a specific header name. The operator, `contains` or `regex`, sets how the value is compared, and the rule ID defaults to `0`, which means every rule. Azion Console calls an exception an allowed rule and keeps it on the rule set's **Allowed Rules** tab.

An exception is a hole in the rule set, as large as you make it. Scoped to one rule, one path, and one named field, it removes one false positive. With the default rule ID and a generic match zone, no family scores anything in that zone, for every request the rule set sees. `unwanted_access` then stops scoring attempts to reach vulnerable or administrative pages and the use of security scanning bots and tools. `identified_attack` stops scoring known attacks against applications and servers.

Write the exception for the false positive you have, and remove it once the application stops sending that request. For every field and match zone an exception carries, refer to [WAF exceptions](/en/documentation/platform/firewall/waf/custom-allowed-rules/#fields).

---

## Tuning

*Logging* and exceptions work as one loop, and the rule set's **Tuning** tab sits in the middle of it. The rule set runs in *Logging* first, so every request it would block is recorded and none is refused. **Tuning** lists those records, grouped by the internal rule that matched, over a window of up to the last 3 days. A query needs a domain and narrows by time range, Network List, IP address, and country, and the details of one rule narrow further by path.

Each record is evidence or a false positive. A real attack shows the rule set doing its job. A legitimate request is a false positive. **Tuning** turns selected records into allowed rules in bulk, so the exception comes from the request that produced it rather than from a guess. When the records stop carrying false positives, the rule moves to *Blocking*.

The loop costs time twice. The window holds 3 days, so evidence nobody reads in time is gone, and a pattern that appears once a month never shows up in it. Bulk conversion is quick because it narrows nothing. It creates a separate rule for each possible attack on each URI it receives, which is wider than an exception written by hand. For the procedure, refer to [Tune a WAF rule set](/en/documentation/guides/application-security/firewall-and-waf/tune-waf/), and for the screen, refer to [WAF exceptions](/en/documentation/platform/firewall/waf/custom-allowed-rules/#tuning).

---

## Where a match is reported

A WAF match leaves nothing in the response, so every question about one is answered from a reporting surface. [Real-Time Metrics](/en/documentation/platform/real-time-metrics/secure-dashboards/#waf) answers how many, in requests processed and requests blocked over time. Real-Time Events answers which request, one row at a time, with the internal rules that matched and the score each family reached. Data Stream answers where else, by sending those events to a system outside Azion, and the GraphQL API queries the same data.

A blocked request is found from the request rather than from the response. It is a `400` whose upstream status is `0`, and its `x-azion-request-id` header finds its row. That lookup is the price of keeping the decision out of the response. A blocked user can report only a **Bad Request** page, and the rule and the score are looked up afterward. For the query, refer to [Find the WAF score of a blocked request](/en/documentation/guides/application-security/firewall-and-waf/how-to-find-waf-score/).

---

## Related resources

- [WAF rule sets](/en/documentation/platform/firewall/waf/rules-set.md): The threat families, sensitivity levels, and internal rules behind WAF scoring.
- [WAF exceptions](/en/documentation/platform/firewall/waf/custom-allowed-rules.md): The fields and match zones of an exception, and the Tuning screen that turns records into exceptions.
- [Tune a WAF rule set](/en/documentation/guides/application-security/firewall-and-waf/tune-waf.md): The procedure that reads what a rule set matched in Logging and turns the false positives into exceptions.
- [Find the WAF score of a blocked request](/en/documentation/guides/application-security/firewall-and-waf/how-to-find-waf-score.md): The query that finds the internal rules and the family scores behind a refused request.
