Web Security

From Response Headers to Behavior: Identifying a WAF

Why I started writing about WAF identification

I used to treat a WAF as a wall. If I sent a payload and received a block page, I would assume that the application was protected and move on. After a few engagements, I realized that I was making a big mistake. A block page tells me that something is filtering traffic, but it does not tell me whether the filter is part of the application, a reverse proxy, a CDN, or a dedicated WAF product. Each of those options behaves differently, and the behavior matters more than the name.

This post is a collection of my notes on how I identify a WAF before I try to bypass it. The process is not about memorizing a list of headers. It is about comparing requests, observing responses, and building a small fingerprint that I can trust. When I know what protection layer I am dealing with, I can decide whether a bypass is worth attempting and which techniques are likely to work.

Start with the normal response

Before I send a single malicious payload, I capture the normal response for the pages I am testing. I record the status code, the headers, the body, and the response time. This baseline is important because a WAF fingerprint is always a comparison. If I do not know what a normal request looks like, I cannot tell what changed when I sent an unusual request.

The normal response also tells me about the stack behind the site. A Server header that says cloudflare is an obvious sign, but the absence of that header does not mean there is no WAF. Many WAF products are configured to hide their presence. A custom reverse proxy might remove the original server header and replace it with its own value, or it might leave no server header at all. I write down what I see so that the next request can be compared with it.

Cookies are another useful signal. Some WAF products add their own cookies to track sessions, challenge states, or client reputation. A cookie that appears only after a suspicious request is worth investigating. A cookie that is present on every response may indicate a managed protection layer that sits in front of the application.

Headers that give the product away

Some WAF products add headers that are easy to recognize. A header such as X-WAF-Profile is a strong indicator that a specific commercial product is involved. Other products add a custom Server value, a challenge identifier, or a header that contains the WAF version. I keep a list of known headers in my notes, but I do not rely on the list alone. The most reliable header observation is a header that appears only when a request is blocked.

I also look for headers that are modified rather than added. If the original application uses X-Powered-By: Express but the response removes that header, something in front of the application is changing the response. That change is a fingerprint even if the WAF product name is not visible. The same idea applies to error pages. If I send a request that should produce a database error, and the response instead shows a generic error page, a protection layer is hiding the application's behavior.

Block pages are more useful than they look

The block page is the part of WAF behavior that most testers ignore. I used to close the browser tab when I saw a block page. Now I save the page, the headers, and the redirect chain. Block pages often contain a product name, a support URL, a reference number, or a challenge identifier. The wording can be unique to a specific vendor. Even a generic block page can be useful if I compare its layout and response code with pages from known products.

The status code matters too. Some WAFs return 403, while others return 406, 429, or 302. A redirect to a CAPTCHA page is different from a direct block. A connection reset is different from a block page. I record the exact behavior because it tells me where the filter is positioned. A reset at the network layer suggests a device-based WAF, while a styled block page suggests a reverse proxy or CDN rule.

The challenge page is a special case. Some WAFs require the client to execute JavaScript, solve a CAPTCHA, or wait a few seconds before the real page loads. That behavior is a fingerprint by itself. It also affects testing because my automated requests may be evaluated differently from a browser request. I need to know whether the challenge is triggered by every request or only by suspicious requests.

Behavioral testing: the part that actually matters

Headers and block pages are only the beginning. The most useful WAF fingerprints come from behavioral testing. I start with a request that is obviously malicious, such as a simple SQL injection string or a script tag, and compare the response with the normal baseline. Then I send variations: different encodings, different case, different parameter names, and different HTTP methods. The way the WAF treats those variations tells me how it normalizes input.

For example, if a normal payload is blocked but the same payload with mixed case is allowed, the WAF is probably doing case-insensitive matching before it checks the parameter. If a payload is allowed as a GET request but blocked as a POST request, the WAF may only inspect certain request parts. If a payload is blocked in one parameter but allowed in another, the rule engine may be applied inconsistently.

Timing is another signal. A WAF that performs complex pattern matching may add a small delay to requests that match certain conditions. I compare the response time of normal requests with the response time of suspicious requests. Timing data is noisy, so I repeat the test a few times and look for a consistent gap. A consistent delay tells me that the filter is processing the request, even if the response looks normal.

The response body is important too. Sometimes a WAF blocks a request but still returns the application's normal page, except for a small marker in the body. Sometimes the application returns an error that the WAF allows through. I read the body instead of just looking at the status code, because the status code can be overridden by the WAF.

Error messages reveal the filter position

Error messages are a useful way to understand where a WAF sits in the request path. If I send a malformed request that should trigger a framework error, the response tells me whether the application code is reached. If the response contains a stack trace or a database error, the WAF allowed the request through. If the response is a generic error page, the WAF may have caught the request first.

The difference matters for bypass work. A WAF that only inspects requests at the edge can be bypassed if the application parses input differently. A WAF that sits inside the application and wraps every controller action is much harder to bypass because it has access to the same normalized data as the application. I use error responses to estimate the filter position.

Another useful trick is sending a request that causes the application to generate a specific error only when a parameter reaches the backend. If I send a value that is invalid for the backend parser and receive a backend error, I know that the value was not blocked. That single observation can save me from building an elaborate bypass for a filter that is not even in the path.

Finding the exact product

Once I have collected headers, block pages, behavior, and timing signals, I try to match the pattern against known WAF fingerprints. Public fingerprint lists contain details such as cookie names, challenge URLs, and default block page text. I compare my observations with those lists and look for a match. The match does not have to be perfect. A combination of two or three independent signals is usually enough to identify the product family.

Version information is harder to find, but it is valuable. Some WAF products reveal their version in a block page, a JavaScript challenge file, or an error response. If I can identify the version, I can search for known bypass techniques and configuration weaknesses. If I cannot identify the version, I at least know which product family I am dealing with.

The product name matters because it changes my strategy. A WAF with a strong rule set but weak parser normalization is different from a WAF with a weak rule set and strong normalization. A product that relies on a known set of signatures can be bypassed with encodings that the signatures do not cover. A product that uses a browser challenge can be bypassed by simulating the challenge, but only if I understand how the challenge works.

Planning a bypass after identification

Identification is not the goal. The goal is to decide whether a bypass is worth attempting. I only start bypass work after I have a clear hypothesis about the WAF's parsing behavior. The hypothesis comes from the observations I collected. If I observed that mixed-case payloads pass, I test case variations more deeply. If I observed that parameter pollution passes, I focus on duplicate parameters.

Each bypass attempt should be treated as an experiment. I send one variation, record the result, and compare it with the baseline. If the variation is blocked, I know that the rule matches that form. If it is allowed, I know that the rule does not match that form. I do not stack ten variations into one request, because then I cannot tell which change made the difference.

The most important rule is that a bypass is only meaningful when it reaches a real vulnerability. Passing a block page is not the same as exploiting SQL injection or XSS. I still need to confirm that the application processes the payload and that the result is visible in the response or the database. I write the full request, the WAF response, and the application response in my notes so that the finding is reproducible.

Common mistakes I make

The first mistake is assuming that a 403 means a WAF blocked the request. The application itself might return 403 for a different reason. I compare the response with the baseline before drawing a conclusion. The second mistake is focusing only on headers. A WAF can be present without any visible header, and a header can be spoofed or copied from another product. The third mistake is using a single payload to test the WAF. The behavior of one payload does not describe the whole rule engine.

The fourth mistake is ignoring the response body. I have seen tests where the status code was 403 but the body contained the application's login page, which meant the request was not actually blocked. The fifth mistake is forgetting to record the exact request. Without the request, I cannot reproduce the observation, and the finding loses its value.

My quick checklist

  1. Save a normal response before sending any malicious input.
  2. Record all headers, cookies, status codes, and response times.
  3. Compare normal and suspicious requests side by side.
  4. Save block pages and challenge pages for later analysis.
  5. Test the same payload with different methods, parameters, and encodings.
  6. Look for timing differences and backend error messages.
  7. Match the collected signals against known WAF fingerprints.
  8. Only start bypass work after forming a hypothesis about parser behavior.

What I would do next time

Next time I want to build the fingerprint before running any automated bypass tool. The automation is useful, but it works better when it starts from a clear model of the protection layer. I also want to spend more time reading the response bodies of blocked requests, because they contain more information than the status code. WAF identification is not a separate phase. It is part of the same conversation I am having with the application.