A UNION-based payload gives you one row per query. A good enumeration pipeline turns that into a complete map of the database - every table, every column, every type, every row count - before you start pulling data. This is the recon phase of a SQLi breach.
- L6UNION-based extraction
- L7Error-based extraction
- Use
information_schema.tablesandinformation_schema.columnsto map the schema. - Discover which tables contain high-value data - users, tokens, sessions, payments - before you start pulling rows.
- Use
COUNT(*)to size a table before extracting it. - Recognise the schema differences between MySQL, PostgreSQL, and SQL Server.
- information_schema
- A virtual schema in every ANSI-compliant RDBMS that exposes the database's own metadata - tables, columns, types, indexes, constraints. The first target of any enumeration.
- Schema mapping
- The process of building a complete picture of the database - every table, every column, every type - before extracting data.
- High-value table
- A table whose contents justify the cost of extraction: users, api_keys, sessions, orders, payments, credentials. The attacker's goal is to identify these tables first.
- row_count estimation
- A pre-extraction COUNT(*) to size the table. A 10-row table takes seconds; a 10-million-row table takes hours and is more likely to be detected.
The metadata that ships with every database
Every modern RDBMS maintains a virtual schema called information_schema. It describes the database's own structure - tables, columns, types, indexes, constraints. The same SQL grammar that lets you query your data lets an attacker query the metadata.
-- Step 1: list tables in the current database
UNION SELECT 1, table_name FROM information_schema.tables
WHERE table_schema = database()
-- Step 2: list columns in a chosen table
UNION SELECT 1, column_name FROM information_schema.columns
WHERE table_name = 'users'
-- Step 3: size the table
UNION SELECT 1, COUNT(*) FROM users
-- Step 4: dump a small sample
UNION SELECT id, username FROM users LIMIT 5The same primitives work in PostgreSQL, SQL Server (where the metadata is exposed via sys.tables and sys.columns), and Oracle (all_tables, user_tables). MySQL is the friendliest target because the metadata is ANSI-compliant and indexed.
Walk the three-step chain
The sandbox fakes a database with four tables - users, api_keys, orders, sessions. Pick the first target, then the column listing, then the dump. The information_schema queries return as if they were real UNION SELECT payloads.
UNION SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema = database()
sql-kw">SELECT id, name sql-kw">FROM products sql-kw">WHERE id = 1 sql-kw">UNION sql-kw">SELECT 1, table_name sql-kw">FROM information_schema.tables sql-kw">WHERE table_schema = database()-- Why attackers always start here
A SQLi finding without a schema map is a foothold. A SQLi finding with a schema map is a breach. Bug bounty payouts for SQLi findings scale with the value of the data the attacker can demonstrate access to - and a complete schema map is the most efficient way to prove that. A good bug report shows the table list, the high-value table names, and a single row from each. That is a P1; a single error message is a P3.
Defenders should expect the same probes. The first request from any scanner after a successful SQLi will hit information_schema.tables. A rule that lets all SELECT queries through but blocks that specific table is the most effective practical detection - the application almost never queries its own metadata.
The fix is the same; the defence-in-depth is sharper
Parameterised queries close the injection - the attacker's payload never becomes SQL grammar. Two defence-in-depth measures make enumeration harder even if a different vector opens the door:
- Use a least-privilege database user for the application. The application should not be able to read
information_schemaat all; the migration tool runs as a different user. - Column-level encryption for PII (email, phone, address) means a successful dump is structurally incomplete - the attacker gets the schema but the values are AES-GCM ciphertext.
// SAFE - the table name is still concatenated because the schema is
// chosen at runtime. Whitelist it.
const ALLOWED_TABLES = ['users', 'orders', 'products'];
if (!ALLOWED_TABLES.includes(tableName)) throw new Error('invalid table');
const query = `SELECT id, name FROM ${tableName} WHERE id = $1`;
await db.query(query, [id]);Even whitelisted identifiers must be checked against a known set - never let the client choose a table or column name without a guard.
What to remember
information_schema.tablesandinformation_schema.columnsare the attacker's first two queries after a successful injection.- Map the schema before you start pulling data. A 10-row table and a 10-million-row table take different amounts of time and noise.
- Use a least-privilege application database user that cannot read
information_schemaas defence-in-depth. - Whitelist any identifier (table, column, sort column) that is chosen at runtime.
Knowledge check
0/3 answered · 0 correct1.Why is the first query after a successful SQLi always information_schema.tables?
2.A bug bounty report shows a single SQL error message containing the table name "users". A second report shows the full information_schema.tables output. Which is the higher-severity finding?
3.Why should the application database user NOT be able to read information_schema?