Skip to content
mediumCVSS 5.3CWE-89A03:2021 – Injection

Filter Evasion

Working around application-level defences: blocked quotes, stripped keywords, escaped input, and length caps — without a WAF in sight.

Application Filters vs. WAFs

WAF bypass deals with a separate appliance inspecting HTTP. This guide covers filters written inside the application: a blocklist in a validator, a str_replace that strips keywords, an escaping routine applied inconsistently.

These are usually weaker than a commercial WAF because they are hand-rolled, but they are also closer to the query, so the constraints are tighter — you often have no control over encoding, and the filter sees exactly what the database will see.

The recurring lesson is that a filter attempts to enumerate badness. SQL's grammar is large enough that the enumeration is never complete.

Injecting Without Quotes

Quote-stripping is the most common hand-rolled defence, and the easiest to work around when you need a string literal.

Hex literals (MySQL). 0x61646d696e is 'admin' with no quote character present.

CHAR() / CHR(). Build the string from character codes. Portable across engines with per-engine syntax.

Concatenation of unquoted values. Column names, numeric literals, and function results can often substitute for the literal you wanted.

Avoid needing a literal at all. Many payloads only need numbers: AND 1=1, ORDER BY 3, UNION SELECT 1,2,3 require no strings whatsoever. Schema enumeration needs strings, but confirming injection does not.

In a numeric context there is no quote to escape in the first place, which is why quote filters give such false confidence.

String literals without quotes

SQL
-- Hex literal — no quotes anywhere
SELECT * FROM users WHERE name = 0x61646d696e

-- CHAR()
SELECT * FROM users WHERE name = CHAR(97,100,109,105,110)

-- Adjacent literal concatenation (MySQL-specific), if partial
-- quoting survives
SELECT 'ad' 'min'

-- Comparing without a literal at all
SELECT * FROM users WHERE name = (SELECT name FROM users LIMIT 1)

-- unhex
SELECT UNHEX(61646d696e)

Exploiting Strip-Based Filters

A filter that removes a matched pattern rather than rejecting the request is worse than no filter, because you can construct input that becomes the payload only after filtering.

If union is stripped once, non-recursively:

Input:    UNIunionON SELselectECT 1,2,3
After:    UNION SELECT 1,2,3

The same trick defeats space-stripping ( ), comment-stripping, and quote-removal. Test for it by sending a doubled keyword and seeing whether the query succeeds.

Related: filters that run in the wrong order. If the application strips comments and then strips keywords, UN/**/ION survives the first pass as UNION and is caught. Reverse the order and it does not. Probing both arrangements is worth two requests.

Defeating Escaping

Applications that escape rather than parameterise fail in specific, well-known ways.

Numeric contexts. Escaping adds backslashes before quotes. In WHERE id = $input there are no quotes, so escaping is a no-op and 1 OR 1=1 passes untouched. This is by far the most common escaping failure.

Charset mismatch. Historically, addslashes() on a connection using a multi-byte charset such as GBK allowed %bf%27 to become a valid two-byte character plus a free quote, consuming the escaping backslash. Fixed by setting the connection charset correctly (mysqli_set_charset), but still present in legacy code.

Double escaping and unescaping. Input escaped, stored, then unescaped on read, then concatenated. See Second-Order.

Escaping the wrong things. An escaper written for string literals does nothing for LIKE metacharacters, identifiers, or ORDER BY targets.

Backslash handling differences. PostgreSQL with standard_conforming_strings on treats backslash literally, so a MySQL-style escaper that turns a quote into backslash-quote leaves the quote intact — it still terminates the literal.

The numeric-context blind spot

PHPproduct.phpVulnerable
<?php
// The developer escaped, so this is "safe".
$id = mysqli_real_escape_string($conn, $_GET['id']);

// But there are no quotes in the query. Escaping quotes
// accomplishes nothing here.
$sql = "SELECT * FROM products WHERE id = $id";

// ?id=1 OR 1=1              -> works, nothing to escape
// ?id=1 UNION SELECT 1,2,3  -> works
// ?id=(SELECT ...)          -> works

Working Within Length Limits

A maxlength on a field or a column width in the database can cap your payload. Short forms help:

-- Instead of: ' OR 1=1-- -   (11 chars)
'||'1              -- Oracle/PG concat trick, 4 chars
' OR 1#            -- MySQL, 6 chars
' OR''='           -- string context, 7 chars, no digits or comment

The ' OR''=' form is worth remembering: it closes the literal, ORs in the comparison ''='' (true), and lets the application's own trailing quote balance the statement, so no comment is needed at all.

Also consider splitting the payload across multiple parameters that are concatenated into the same query — each stays under the limit, and the query assembles them for you. This is common in search forms that build a WHERE clause from several fields.

Why Blocklists Lose

Consider the effort asymmetry. To block injection with a blocklist you must enumerate every keyword, every alias, every encoding, every comment form, and every whitespace variant, across every engine, forever — and you must not break legitimate input containing the word "union" or an apostrophe.

To bypass it you need to find one gap.

Concretely, blocking SELECT still leaves ORDER BY oracles, boolean inference, and time-based extraction, none of which require the keyword. Blocking quotes leaves numeric contexts and hex literals. Blocking spaces leaves parentheses and comments. Blocking information_schema leaves sys.*, mysql.innodb_table_stats, and error-based schema leakage.

This is why the entire industry consensus is parameterisation. Not because filters are useless, but because they are unbounded work with a bounded adversary cost.

Prevention

  • Parameterise. Once values are bound, none of this page applies.
  • Reject, never sanitise. If input fails validation, return an error. Silently stripping characters creates the strip-based bypasses above and corrupts legitimate data.
  • Validate positively. "Must match ^[0-9]+$" is a complete defence for a numeric ID. "Must not contain SELECT" is not a defence for anything.
  • Allowlist identifiers. The one place parameterisation cannot reach.
  • Set the connection charset explicitly if any legacy escaping remains, to close the multi-byte class.
  • Never rely on maxlength or client-side validation. Both are advisory.
  • Turn off prepared-statement emulation in PDO so binds happen in the database rather than in string-building code.