SQL Injection
A login form that trusts raw input can hand an attacker the whole database.
Builds on
What you will learn
- -Explain how string-concatenated input turns SQL data into executable code
- -Bypass a login with authentication payloads like admin' -- and ' OR 1=1 --
- -Read other tables with a UNION query and extract data blind when nothing prints
- -Choose parameterized queries and least privilege as the defenses that actually hold
The OWASP Top 10 lesson ranked injection at A03 and flagged it as the one class worth studying on its own. This is that study, and is its canonical form. A single quote typed into a login field, and the application answers with a 500 error. That error is the whole tell. It means the quote broke the SQL statement the was assembling, which means the typed input reached the query as code instead of as data. SQL injection is the name for that failure: an application builds a database query by gluing user input into a string, and an attacker supplies input that rewrites what the query means.
Data that becomes code
A relational database speaks SQL, and a login check is one small query. The server asks the database whether a row exists for a given username and , and the database answers.
SELECT * FROM users WHERE username = 'admin' AND password = 'hunter2';
The quotes carry the whole security model. Everything between a pair of single quotes is a string literal, plain data the database compares against. The keywords outside the quotes, SELECT, WHERE, AND, are code the database executes. That boundary between quoted data and unquoted code is what keeps input from becoming instructions, and string concatenation erases it.
Consider how a careless application assembles that statement. It takes the username field, drops it between two quotes, and appends the .
query = "SELECT * FROM users WHERE username = '" + input + "' AND password = '" + pw + "'";
Whatever the input field holds lands inside those quotes. An ordinary name changes nothing. A name that contains its own quote closes the string early and spills into the code , where the database reads it as SQL. At that point the attacker is no longer filling in a value. The attacker is writing part of the query.
Authentication bypass
The first every tester reaches for is the comment. The characters -- begin a comment in SQL, so the database ignores the rest of that line. A username of admin'-- builds this:
SELECT * FROM users WHERE username = 'admin'--' AND password = '';
The injected quote closes the username string, and the comment swallows the password check whole. The database sees a query that asks only whether a user named admin exists, answers yes, and the application the attacker in as admin with no password at all.
The second classic payload does not need a real username. ' OR 1=1 -- closes the string, adds a condition that is always true, and comments out everything after it. Because 1=1 holds for every row, the WHERE clause matches the entire users table, and a login routine that takes the first row returned usually hands back the . Both payloads the same crack: the quote in the input escapes the data region, and the SQL that follows runs.
The login form below is genuinely vulnerable, running against a real database sandboxed inside the browser, and it prints the exact query it builds as each payload lands, so the break stays visible as it happens.
The query it runs
SELECT * FROM users WHERE username = '...' AND password = '...'Your input is dropped straight into the query. Whatever you type becomes part of the logic.
Submitting admin'-- into a concatenated login query logs an attacker in as admin without a password. What does the -- accomplish?
Confirm it first
A tester confirms the injection point before spending time on payloads. The fastest tell is the single quote: a ' placed in a , followed by a database error, a broken page, or a changed response, shows the input is reaching the query. Boolean logic then proves it. A condition that is always true and one that is always false, sent through the same parameter, produce different responses only when the database is evaluating the input as logic.
$id=1' OR '1'='1$id=1' AND '1'='2Different responses to those two inputs mean the database is acting on the injected logic. That is confirmation, and it is worth having before moving to extraction.
UNION: read tables the app never meant to show
When the application prints query results on the page, UNION is the fastest path to the rest of the database. UNION SELECT staples a second query onto the first, so the attacker chooses which data comes back in place of the original row.
Two conditions have to hold before a UNION works. The column count must match, and the data types have to line up. The column count is found with ORDER BY: the number climbs until the database errors, and the last number that did not error is the count.
$id=1 ORDER BY 3-- -$id=-1 UNION SELECT 1,2,3-- -$id=-1 UNION SELECT 1,username,password FROM users-- -Setting id=-1 makes the original row return nothing, so only the injected rows appear on the page. database(), version(), and @@version are the orientation queries a tester runs once a working UNION exists, because they name the schema and the engine and decide which syntax the rest of the attack uses.
Your UNION SELECT payload keeps returning a column-count error. What do you find first?
When nothing prints: blind injection
Plenty of applications run the injected query but never show its result. The attack still works, one bit at a time. Boolean-blind injection asks a yes or no question and reads the answer from whether the page changes. Time-blind injection makes the database sleep when a condition is true, and reads the answer from the clock.
$id=1' AND (SELECT SUBSTRING(version(),1,1))='8'-- -Time-based injection is the fallback for when even the true and false pages look identical. A payload like id=1' AND SLEEP(5)-- - makes a vulnerable MySQL backend hang for five seconds. A five-second delay that toggles on and off with the condition is proof, and the same delay is the channel the data leaks through. The method is slow by hand, which is why sqlmap exists: it automates the character-walking once a tester has confirmed the injection manually.
What the attacker actually walks away with
The login bypass is the demo. The prize is the rest of the database. A working UNION or a blind oracle reads any table the database account can reach: password hashes, session , email addresses, payment records. information_schema lists the name of every table and column, so an attacker maps the schema first, then drains whatever looks valuable.
Some backends go past reading. Microsoft SQL Server historically exposed xp_cmdshell, which runs operating- commands from inside a query and turns a data leak into code execution on the . PostgreSQL can write files through its COPY command, and MySQL through INTO OUTFILE, which an attacker aims at a web-writable to drop a script the server will later run. Stacked queries, where a semicolon starts a second statement, let an attacker run INSERT, UPDATE, or DROP when the driver allows it. Injection that reaches those features stops being a database problem and becomes full server compromise.
The fix that actually holds
Escaping and filtering are the tempting answer and the wrong one. A blocklist that strips quotes or the word SELECT looks reasonable until an attacker sends SeLeCT, or a numeric parameter with no quote to break, or a comment sequence the filter never anticipated. Double - and engine-specific syntax defeat blocklists faster than anyone can extend them. Filtering treats the symptom and leaves the disease in place: input is still being interpreted as code.
The real fix separates code from data so completely that input can never cross back. A parameterized query, also called a prepared statement, sends the SQL text and the values on separate channels. The database compiles the query with placeholders first, then binds the values as pure data that cannot change the query's structure. A quote inside a bound value is only a quote. ' OR 1=1 -- becomes a username the database searches for and fails to find. Every mainstream language ships this: PreparedStatement in Java, parameterized cursors in Python's DB-, placeholders in Go's database/sql, bound parameters in any ORM. Concatenating input into SQL is the anti-pattern; binding it is the pattern.
is the second layer, for the day the first one slips. The application's database account rarely needs to read information_schema, drop tables, or write files to disk. An account scoped to the specific tables and operations the app uses turns a catastrophic dump into a contained one. The layers : parameterized queries stop the injection, least privilege limits the blast radius if one query is ever missed, and suppressing raw database errors slows an attacker without pretending to be a fix on its own.
An app concatenates user input into every SQL query and is riddled with injection. Which change actually removes the vulnerability?
Spin up DVWA in a local , or open OWASP Juice Shop in your browser. Both are built to be attacked safely. On the DVWA SQL Injection page, set the security level to low, type a single quote into the ID field, and watch for the database error that confirms your input reaches the query. Then submit ' OR '1'='1 and read how the response changes once your condition is always true. Raise DVWA to medium and try again: note which of your payloads the added escaping blocks and which slip through, and write down why. Finish on PortSwigger's Web Security Academy SQL injection labs, which take you from a UNION column-count probe to a full data extraction against a target that is yours to break. Keep every payload on these lab targets.
Every payload in this lesson belongs on systems the tester owns or has written permission to test: a local DVWA or Juice Shop container, a HackTheBox machine, a PortSwigger Academy lab. Sending these at a site without explicit is a computer-crime offense in most countries, and a real 500 error on someone else's login form is not an invitation.
The database was the target here because the application trusted input as SQL. The next class of bug trusts input as . Cross-site scripting takes the same broken boundary between data and code and moves it into the victim's browser, where the injected payload runs as instead of a query.