Web Security

SQL Injection and WAF Bypass: Common Techniques

Why I started collecting WAF bypass notes

SQL injection used to feel straightforward. I sent a single quote, saw an error, and then tried to extract data. The process changed when I started testing applications behind a WAF. The single quote still reached the application, but the full payload was blocked before it could do anything useful. I needed a different way to think about the problem.

This post is my personal study note on SQL injection and WAF bypass. I am writing it because the techniques are easy to list but hard to use correctly. The most important lesson is that a bypass is not a magic string. It is a hypothesis about how the WAF and the backend parser disagree. If I do not understand that disagreement, I am just guessing.

Start with the normal query context

The first thing I need to understand is where the input goes. I look at the application code, the API documentation, and the error behavior to determine whether the input is used in a WHERE clause, an ORDER BY clause, a LIMIT, a JOIN, or an INSERT statement. The query context determines which syntax is useful.

For example, if the input is inside a string literal, I need to break out of the string with a quote. If the input is a numeric value, I do not need a quote. If the input is used in an ORDER BY clause, I might not be able to use a UNION-based payload in the same way. The context tells me which payload shapes are worth testing.

I also need to know which database is in use. MySQL, PostgreSQL, Oracle, and SQL Server have different comment syntax, different string concatenation, and different functions. A payload that works on MySQL may do nothing on PostgreSQL. The database type can often be inferred from the error message, the default page, or the application behavior.

How a WAF normalizes input

A WAF does not look at the raw request in the same way that the backend does. It decodes, normalizes, and transforms the input before applying its rules. The order of these steps is important. If the WAF decodes URL encoding once, but the backend decodes it twice, there is a window where the WAF sees one value and the backend sees another.

The same idea applies to character sets. If the WAF assumes UTF-8 but the database connection uses a different charset, a byte sequence can be interpreted differently. The WAF might not match a signature because the signature is written for the decoded form, while the database sees a dangerous character.

I start by sending harmless versions of the input to map the normalization. I send the same value with different encodings and compare the response. If the response changes, I know that the application decoded the value differently. If the response does not change, the value may be normalized before it reaches the query.

Comments and whitespace

Inline comments are one of the oldest WAF bypass techniques. In MySQL, // can replace spaces, and keywords can be split with comments. A payload like 1'//UN//ION//SEL/**/ECT can break a signature that expects the words UNION SELECT to appear next to each other.

The trick only works when the database accepts the comments in that position. Different databases have different comment syntax and different rules for where comments can appear. I test the comment form against the actual query behavior instead of assuming it will work.

Whitespace is another area where filters can be bypassed. Some filters remove spaces, but they do not remove tabs, newlines, or other unusual whitespace. I test whether the backend accepts these characters in the same places as spaces. If it does, I can use them to break a signature.

Encoding and double decoding

URL encoding is the first encoding that most people try. A payload like %27 is the encoded form of a single quote. If the WAF decodes the input once and then matches a signature, the encoded form might avoid the match. The backend, however, may decode the input again before using it.

Double URL encoding is a variation where the percent character itself is encoded. The WAF may decode once and see %27, while the backend decodes again and sees a single quote. This technique is worth testing, but it only works when the full request path performs multiple decoding steps.

Unicode normalization is another encoding issue. Some databases and frameworks normalize certain characters before processing them. A payload that uses a Unicode lookalike for a quote or a semicolon might pass the WAF signature and then be normalized into a dangerous character by the backend. The behavior depends on the database collation and the application framework.

Equivalent functions and operators

Keywords are the easiest part of a signature to avoid. If the WAF blocks the word SELECT, I can look for an equivalent function, operator, or syntax that produces the same result. String concatenation can build keywords at runtime. Hex literals can represent values without using letters that trigger a signature.

Boolean logic can often be expressed without the words AND and OR. Comparison operators, arithmetic, and bitwise operations can be used to build a boolean expression. The exact syntax depends on the database, so I keep a small reference table in my notes.

The goal is not to make the payload as complex as possible. The goal is to make the payload express the same operation in a form that the signature does not match. A minimal payload is easier to test and easier to explain.

Parameter pollution

Parameter pollution is one of my favorite techniques because it exploits a real parser difference. I send the same parameter twice, once with a clean value and once with a dangerous value. The WAF may inspect only the first value, while the application framework uses the last value, or the other way around.

The behavior depends on the web server, the reverse proxy, and the framework. I test the order of the parameters and observe which value reaches the backend. If the application returns an error that mentions the second value, the framework is using the second value. If it returns a response that matches the first value, the framework is using the first value.

Parameter pollution is useful when every encoding-based technique is blocked. It does not rely on a decoding bug. It relies on the difference between what the WAF sees and what the application sees.

Timing and boolean-based testing

Sometimes I do not need to bypass the WAF at all. If the WAF allows a simple boolean expression, I can use time-based and boolean-based testing to confirm the injection without sending a large payload. A time-based payload like SLEEP(5) may be blocked, but a boolean payload like 1=1 may be allowed.

Boolean-based testing works by comparing the response when the condition is true with the response when the condition is false. If the responses differ, the input is affecting the query. I use this technique to confirm the injection and to learn about the database before I try to extract data.

Timing-based testing uses a delay to confirm the injection. I send a payload that should cause the database to sleep, and I measure the response time. If the response is delayed by the expected amount, the payload reached the database. Timing tests are noisy, so I repeat them and compare the results.

Verifying that a bypass is real

The hardest part of WAF bypass testing is knowing when a bypass is real. A payload that is not blocked is not necessarily useful. The payload must change the query behavior, return data, or create a measurable side effect. I confirm the behavior with a simple test before I build a full extraction chain.

The verification starts with the query context. I use a payload that should change the response in a predictable way, such as making a condition true or false. If the response changes, the payload reached the database. If it does not change, the input may be filtered, validated, or used in a different part of the query.

I also verify that the result is reproducible. I send the same request twice and compare the responses. A one-off difference is probably noise. A consistent difference is evidence that the injection works.

Blind injection details

Blind injection is slower than a visible error, but it is often the only option. I use two main techniques: boolean and time-based. The boolean technique compares the response when the condition is true with the response when it is false.

The condition can be simple at first, such as 1=1 versus 1=2. Once I confirm the difference, I use the same pattern to extract data one character at a time. The extraction is slow, but it is reliable.

The time-based technique uses a delay to confirm the injection. A payload like IF(1=1,SLEEP(3),0) should delay the response. The delay can be measured and compared with a baseline.

The blind techniques work through a WAF only when the WAF allows the basic expressions. I test the smallest possible payload first and build from there.

Union and error-based basics

The union technique is the fastest way to extract data, but it requires a visible response. I start by counting the columns in the original query. The number of columns determines the union payload.

The error-based technique uses the database error messages to extract data. A function that includes the data in the error message can return one value at a time. The technique requires the error to be visible in the response.

The two techniques have different WAF behavior. A union payload contains the word UNION and is often blocked. An error-based payload may use a function that is not in the signature list.

I test the visible techniques before the blind techniques. The visible result is easier to verify and easier to explain.

Database fingerprinting

The database type determines the syntax. I can fingerprint the database with a few requests. The error message is the easiest source, but it is not always visible.

The version function is another source. Each database has a different function name, such as version(), @@version, or V$VERSION. I test the functions that are allowed by the WAF.

The string concatenation also differs. MySQL uses CONCAT, PostgreSQL uses ||, and SQL Server uses +. A payload that uses the wrong operator will not work.

The fingerprint tells me which bypass techniques are worth trying. I keep the database reference in my notes and update it after every test.

Common mistakes I make

My biggest mistake is spending too much time on a bypass before confirming the injection. I should confirm that the parameter reaches the query first. My second mistake is using a payload that is too complex. The more pieces a payload has, the harder it is to tell which piece matters. My third mistake is assuming that a WAF bypass works on every database. The syntax is database-specific.

The fourth mistake is forgetting to record the WAF response and the application response separately. Without that record, I cannot explain why the bypass worked. The fifth mistake is treating the WAF as the application. A bypass that reaches the database is not a vulnerability unless the database processes it.

My quick checklist

  1. Identify the query context and the database type.
  2. Map the WAF normalization with harmless inputs.
  3. Test comments, whitespace, encodings, and equivalent functions.
  4. Test parameter pollution and parser differences.
  5. Confirm the injection with boolean or timing behavior.
  6. Keep the payload minimal and reproducible.
  7. Record the WAF response and the application response separately.
  8. Report only verified injection points.

What I would do next time

Next time I want to confirm the injection before I spend time on bypasses. The confirmation tells me which part of the payload actually matters and gives me a target for the bypass work. I also want to keep a better reference table of database-specific syntax, because that is where I waste the most time. SQL injection testing is not about collecting payloads. It is about understanding how the input moves through the request path.