Waffle

[[Waffle]] is a Web Application Firewall (#waf). Most WAFs ship with a fixed set of rules and match each request against them, so they only catch attacks someone has already written a rule for. WAFFLE takes the other route. It learns what normal traffic looks like for one specific web application and treats anything that strays too far from that as an attack. Because the decision rests on #anomaly-detection instead of a signature list, it catches both known and unknown #web-security attacks.

[[WAFFLE]] is written in #python and runs as a #proxy between the client and the web server. It could also live inside the server as a module, but running it as a proxy keeps it independent of the web platform behind it, so the same firewall can sit in front of any stack.

It works in two modes:

* **Firewall.** Bad requests are dropped before they reach the server, and the user gets a 403 page back.
* **Detection (#ids).** The proxy only watches. It analyzes each request, writes an event when something looks suspicious, and otherwise stays out of the way.

## Architecture

[[WAFFLE]] has four parts: the proxy, the filters, an event and log database, and a management interface.

```mermaid
flowchart LR
Client[Client] -->|request| Proxy[WAFFLE Proxy]
Proxy -->|allowed| Server[Web Server]
Server -->|response| Proxy
Proxy --> Filters[Filter Chain]
Filters <-->|compare| Model[(Behavior Model)]
Filters -->|events & logs| DB[(Event / Log DB)]
Admin[Management Interface] --> DB
```

## The behavior model

Before [[WAFFLE]] can block anything, it needs a picture of what normal looks like. That picture is a graph. Each node is a page, and each edge is a transition a real user made from one page to another. Every edge also keeps a count of how often that transition happened, so the graph turns into a statistical model of how people actually move through the site.

```mermaid
graph LR
Home["/"] -->|count=42| Login["/login"]
Home -->|count=180| Product["/product/view"]
Login -->|count=40| Account["/account"]
Product -->|count=95| Cart["/cart"]
Cart -->|count=38| Checkout["/checkout"]
```

When a request comes in, the system compares it against this model. If the difference crosses the configured thresholds, the request is flagged as an attack and an alert fires. In firewall mode the request is also blocked and the user is sent a 403 page.

The transition control filter is the clearest example. Say a user jumps from `/` to `/login`. The filter checks whether an edge for that transition exists in the graph. If there is no edge, it raises an event. If the edge does exist, WAFFLE runs the traverse possibility filter to see whether the target is a rarely visited node, which on its own can be a sign of probing.

## Filters

Detection is a chain of filters. Each one inspects a different part of the incoming request and checks it against the behavior model. A filter carries its own metadata: a reason message, an event code, a severity, a priority, a block-or-pass flag, and the parameters it needs. It also holds two methods: `perform`, which runs the actual check, and `action`, which runs when the check trips.

The traverse possibility filter is a good walk-through. It calculates the traverse possibility between the user's last node and current node, then checks whether the threshold is exceeded. If it is, `perform` returns `True` to the control manager, and the control manager runs the filter's `action` with the new event object. An action can do something useful, like add the user's IP to a blacklist table.

The filters lean on a fixed set of HTTP properties pulled from each request:

| Property | Example |
| ------------------- | ---------------------------------------------------- |
| Path | `/product/view` |
| URI | `/product/view?id=25` |
| Verb | `GET`, `POST` |
| Response code | `200`, `301` |
| Parameter keys | `msg=`, `email=` |
| Parameter values | `'hello'`, `'example@example.com'` |
| Parameter sizes | `len('msg')`, `len('email')` |
| Parameter entropies | `entropy('hello')`, `entropy('example@example.com')` |

These are the filters shipped with the system:

| Filter | Code | Severity | Priority | Blocks |
| -------------------- | ---- | -------- | -------- | ------ |
| SessionLimit | SLE | Critical | First | Yes |
| PassiveDetailsChange | PDC | Low | Low | No |
| LinkBetweenNodes | LBN | Critical | High | Yes |
| RequestExistence | REA | Critical | High | Yes |
| TraversePossibility | TRP | Medium | Mid | No |
| ParamValueLength | PLA | Low | Low-Mid | No |

## Request life-cycle

Every request runs through the same four stages, in order.

```mermaid
flowchart TD
A[Incoming request] --> B[Session handling]
B --> C[Graph edge controls]
C --> D[HTTP property filters]
D --> E[Traverse possibility controls]
E --> F{Anomalous?}
F -->|yes| G[Event, then block or log]
F -->|no| H[Pass to web server]
```

Session handling comes first and keeps the proxy stable. [[WAFFLE]] creates a unique session id and stores it in the user's browser as a cookie, then checks that cookie on every request. If the value does not come back correctly, it issues a new one. Once the session is settled, the filters run in order of priority.

## Management interface

[[WAFFLE]] ships with a #flask management interface for the day-to-day work: browsing events and logs, inspecting nodes in the behavior graph, and banning an IP address by hand.

![Management interface dashboard](/images/waffle/dashboard.webp)