Sometimes the database wants to tell you the answer - let it. Verbose error messages in development and staging environments are one of the most efficient ways to extract data, because the database does the work of fetching it and you only have to read the response.
- L1Fundamentals
- L3Why it's possible
- Trigger database errors that surface the value of a subquery.
- Use
CAST,EXTRACTVALUE,UPDATEXML, and Oracle'sCTXSYSfamily. - Recognise when an application is too verbose in production to be safe.
- Know why error-based extraction only works when error messages are visible to the attacker.
- Verbose error
- A database error message that includes the offending value, column, table, or query. The default in many development environments; usually suppressed in production.
- CAST failure
- Forcing a value to the wrong type. The database fails and prints the original value in the error - turning a runtime error into a data exfiltration channel.
- XPATH error
- EXTRACTVALUE and UPDATEXML expect a valid XPATH. Passing a concatenated string makes MySQL surface the string in the XPATH error message.
- Side channel
- An information leak that isn't the intended response. Errors are the most common side channel in SQLi; timing is another (covered later).
When the database tells you what you asked
Most databases include the offending value in the error message when a query fails. The simplest exfiltration is to put the data you want inside an expression that is guaranteed to fail.
-- MySQL: trying to cast a string version() to INT fails
-- and surfaces the version string in the error.
SELECT * FROM products WHERE id = 1 AND 1=CAST((SELECT version()) AS INT);
-- PostgreSQL: same trick, same result
SELECT * FROM products WHERE id = 1 AND 1=CAST((SELECT current_user) AS INTEGER);This works in development. In production, error messages are usually swallowed and replaced with a generic 500. The next three lessons cover the techniques that work even when the application is silent. But when error messages are visible - which they almost always are in staging, dev, and many SaaS UIs - error-based extraction is faster than anything else.
Five database primitives
Cycle through the templates. The query panel shows the payload the application builds; the response panel shows what the database returns. Five templates cover the four major engines - MySQL, PostgreSQL, and Oracle.
MySQL refuses to cast a string version ("8.0.32-0ubuntu0.20.04.2") to an integer and surfaces the value in the error.
SELECT * FROM products WHERE id = ' AND 1=CAST((SELECT version()) AS INT)-- When production leaks anyway
Even in production, error messages leak through:
- Sentry and log dashboards. The application returns a generic 500 to the user, but the developer sees the full stack trace - including any value the attacker coerced into the query.
- Error-tracking webhooks.Tools like Sentry, Datadog, and Bugsnag forward error events to third-party services. A well-timed attack can exfiltrate data into the third-party's log search.
- Verbose 500 pages. Many frameworks (Django in
DEBUG=True, Rails, Laravel in development) print the failing query in the HTML body of the 500 response.
What kills error-based extraction
Two changes matter. First, the application must never reflect database error messages in the response. Catch the exception, log it server-side, return a generic 500. Second - and this is the important one - parameterise the query. A bound parameter cannot become a CAST argument because the parser never sees it as SQL.
// SAFE - bound parameter; the value never reaches the parser as code
const query = 'SELECT * FROM products WHERE id = $1';
await db.query(query, [id]);What to remember
- Error-based extraction works when the database includes the offending value in its error message.
- The four canonical primitives are
CAST(any DB),EXTRACTVALUEandUPDATEXML(MySQL), and theCTXSYSfamily (Oracle). - Production is not always safe - log dashboards, Sentry, and verbose framework defaults all leak.
- The real fix is the same as lesson 1: parameterise. The error channel becomes a generic 500 with no useful payload.
Knowledge check
0/3 answered · 0 correct1.Why does CAST((SELECT version()) AS INT) leak the version string?
2.You trigger a CAST error in production. The user sees a generic 500, but Sentry captures the full error. Is error-based extraction still possible?
3.Which MySQL primitives are commonly used for error-based extraction?