Error-based
Error-based SQL injection leaks data via verbose error messages from the database. Server returns the result inside the error string. Fix: parameterise + suppress detailed errors in production.
Anatomy, examples, defences
EXAMPLE
# 1) Vulnerable shape
# Node — pg
const sql = `SELECT id, name FROM users WHERE id = ${req.query.id}`;
await db.query(sql);
# Attacker probes with a malformed input:
# ?id=1' AND 1=CAST((SELECT current_database()) AS int) --
# The CAST fails with: 'invalid input syntax for type integer: "myapp_prod"'
# The DB error LEAKS the current database name to the attacker.
# 2) Postgres example payloads
# Get the version
# ?id=1' AND 1=CAST(version() AS int) --
# → error: 'invalid input syntax for type integer: "PostgreSQL 16.1..."'
# Get table names
# ?id=1' AND 1=CAST((SELECT array_agg(table_name) FROM information_schema.tables) AS int) --
# Get column names of a table
# ?id=1' AND 1=CAST((SELECT array_agg(column_name) FROM information_schema.columns WHERE table_name='users') AS int) --
# Get data
# ?id=1' AND 1=CAST((SELECT password FROM users WHERE id=1) AS int) --
# 3) MySQL — extractvalue / updatexml leak via XML errors
# ?id=1' AND extractvalue(1, concat(0x7e, (SELECT version()))) --
# → error: 'XPATH syntax error: '~8.0.36''
# ?id=1' AND updatexml(1, concat(0x7e, (SELECT password FROM users LIMIT 1)), 1) --
# 4) MSSQL — convert / cast errors
# ?id=1' AND 1=convert(int, db_name()) --
# → error: 'Conversion failed when converting the nvarchar value "myapp_prod" to data type int.'
# Or stack queries (if multiple statements allowed):
# ?id=1'; THROW 50000, (SELECT TOP 1 password FROM users), 1; --
# === Defences ===
# 5) Parameterise — closes the door entirely
const { rows } = await db.query(
'SELECT id, name FROM users WHERE id = $1',
[req.query.id],
);
# Even better, validate the input shape first:
const id = z.coerce.number().int().positive().parse(req.query.id);
# 6) Hide error details in production
# Application-level
import pg from 'pg';
try {
await db.query(sql, params);
} catch (e) {
log.error({ err: e, sql, params }, 'db_error'); // server-side log
return res.status(500).json({ error: 'internal' }); // generic for client
}
# NEVER return e.message to the client.
# 7) Database error verbosity — Postgres
ALTER SYSTEM SET log_error_verbosity = 'terse';
SELECT pg_reload_conf();
# (Affects server logs; app still controls client-facing messages.)
# 8) Restrict the DB user
GRANT SELECT, INSERT, UPDATE, DELETE ON specific_tables TO app_user;
REVOKE ALL ON information_schema FROM app_user;
# Now even if injection works, attacker can't enumerate schemas.
# In MySQL — separate user with minimum privileges:
GRANT SELECT, INSERT ON myapp.* TO 'app_user'@'%';
REVOKE ALL PRIVILEGES ON mysql.* FROM 'app_user'@'%';
# 9) Use an ORM / type-safe query builder
# Prisma — typed queries; no string SQL by default
await prisma.user.findUnique({ where: { id: Number(req.query.id) } });
# Drizzle / SQLAlchemy / Diesel — same idea
# These libraries make string-built SQL the exception you have to opt into.
# 10) Monitor for SQLi probes
# Log anomalies:
# - Requests with SQL keywords (CAST, CONVERT, UNION, SELECT, --, etc.) in unexpected params
# - Many 500s from one IP/UA
# - Errors with stack traces leaking schema info
# Forward to SIEM; alert on patterns.
# 11) WAF — defence in depth
# Cloudflare / AWS WAF / Imperva have generic SQLi signatures.
# NOT a substitute for parameterisation, but good last-line defence.
# === Real-world checklist ===
# 12) Audit your code
rg -t js -e 'query\(\s*`' -e 'query\(\s*"' -e 'execute\(\s*f' src/
rg -t py -e 'execute\([^,)]+%' -e 'f"SELECT' app/
rg -t php -e 'query\("SELECT \.$' -e 'query\("INSERT.*\.$'
# Identify every string-built query. Either parameterise it or document why it's safe (e.g., constant SQL).
# 13) Pre-prod testing
# Run sqlmap against your staging env (authorised):
sqlmap -u 'https://staging.example.com/users?id=1' --batch --level=3 --risk=2
# Or use Burp Active Scan / OWASP ZAP / Nuclei templates
# 14) Static analysis
# semgrep + custom rule to flag string concat in SQL queries
# CodeQL has built-in injection detection
# 15) Database-side hardening
# - Disable verbose errors in production
# - Restrict app user permissions (no DROP, no information_schema)
# - Set statement_timeout to bound runaway queries
# - Enable audit logging (pg_audit, MySQL audit plugin)
# 16) What 'error-based' tells you about your code
# If an attacker can leak DB content via errors, the SAME bug usually allows:
# • UNION-based SQLi (extract data directly)
# • Time-based SQLi (when errors are suppressed)
# • Boolean-based SQLi (infer truth from response differences)
# • Even RCE in some scenarios (xp_cmdshell, COPY FROM PROGRAM, etc.)
# Fix the parameterisation; ALL injection variants close together.
# 17) Common mistakes
# ❌ Sanitising errors but keeping string concatenation in SQL → blind / time-based still works
# ❌ Trusting WAF without fixing the code
# ❌ Disabling error messages but leaving stack traces in logs accessible via debug endpoints
# ❌ Allowing the app user to access information_schema (or sys / pg_catalog)
Why it matters
Error-based SQLi is one symptom of a string-built query. Parameterise to close the bug, suppress error details in production, and run static analysis in CI — all three layers harden against the variants you didn’t test for.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Forces DB errors that leak data (XPath / CAST tricks). // Defence: do NOT show DB errors to users; generic 500 + log internally.Try it Yourself »
Discussion
Loading…