Classic (in-band)
Classic in-band SQL injection patterns, explained defensively. What they look like in vulnerable code, how parameterisation closes them, and detection rules to add.
SQLi — classic patterns, defensively
EXAMPLE
-- SCOPE: defensive learning on the bundled lab app. Authorised testing only.
-- ===== Why 'classic' =====
-- In-band SQLi: the response carries the result of the injected query.
-- Variants: union-based, error-based, stacked queries, boolean / numeric escapes.
-- ===== Pattern 1: numeric context =====
-- Vulnerable shape (DO NOT do this):
-- sql = 'SELECT * FROM products WHERE id = ' + req.params.id
-- If id = '1 OR 1=1' -> returns all rows.
--
-- Fix:
-- db.query('SELECT * FROM products WHERE id = $1', [Number(req.params.id)])
-- ===== Pattern 2: string context =====
-- Vulnerable:
-- sql = "SELECT id FROM users WHERE email = '" + email + "'"
-- Payload: ' OR '1'='1
--
-- Fix:
-- db.query('SELECT id FROM users WHERE email = $1', [email])
-- ===== Pattern 3: UNION-based =====
-- Used to read columns from other tables when the response shows rows.
-- Vulnerable code allows the response to grow with injected UNION SELECT clauses.
--
-- Fix: parameterise + return ONLY the columns the app needs (DTOs, not SELECT *).
-- Use a row mapper that strips unexpected columns.
-- ===== Pattern 4: error-based =====
-- The DB error leaks state. Vulnerable code echoes DB errors verbatim.
--
-- Fix:
-- - Generic 500 messages to the client
-- - Detailed errors only to internal logs
-- - Disable verbose DB errors in prod (Postgres: log_min_error_statement = error)
-- ===== Pattern 5: stacked queries =====
-- Some drivers / engines allow semicolons:
-- sql = 'SELECT 1; DROP TABLE users;'
--
-- Fix:
-- - Disable multi-statement support at the driver where possible
-- - Parameterised queries do not interpret semicolons in user data
-- ===== Pattern 6: ORDER BY / LIMIT / column names =====
-- Parameters bind VALUES, not identifiers. So ORDER BY is special.
-- Defence: ALLOWLIST.
const ALLOWED_SORT = new Set(['name', 'created_at', 'total_cents']);
const col = ALLOWED_SORT.has(req.query.sort) ? req.query.sort : 'created_at';
const sql = \`SELECT id FROM orders ORDER BY ${col} DESC LIMIT $1\`;
db.query(sql, [limit]);
-- ===== Detection rules (for SOC / EDR) =====
-- 1. Spike in SQLSTATE 42* (syntax) from a single IP / user
-- 2. Same route receiving wildly different query SHAPES
-- 3. Long URLs / body fields with quotes + UNION / AND / OR keywords
-- 4. Error rate increase isolated to one endpoint
-- 5. Unusual outbound DNS from DB host (out-of-band data exfil)
-- ===== Defence layers =====
-- 1. Parameterise EVERY query (the core)
-- 2. Least-privilege DB roles per service responsibility
-- 3. WAF rules on query string + body patterns (defence in depth)
-- 4. Logging shape: route + user_id + template + error_class
-- 5. Statement timeouts (prevents long blind SQLi probes)
-- Postgres:
ALTER ROLE app_auth SET statement_timeout = '2s';
-- MySQL:
SET SESSION MAX_EXECUTION_TIME = 2000;
-- ===== Recovery checklist if you find a real one =====
-- 1. Patch (parameterise) and deploy
-- 2. Audit logs for likely exploitation IPs + timestamps
-- 3. Rotate any credentials or tokens potentially exposed
-- 4. Add a regression test exercising the previously-bad input
-- 5. Communicate per your incident response plan
-- ===== Patterns to internalise =====
-- - Parameterise EVERY query; no exception is small
-- - Generic error messages to clients; detailed errors to logs
-- - Allowlist for any dynamic identifier (ORDER BY, column names)
-- - statement_timeout per service role to bound blast radius
-- ===== Pitfalls =====
-- - 'It is an integer' — still parameterise; never concatenate
-- - Multi-statement support enabled in driver because it was the default
-- - Echoing 'syntax error near X' to clients
-- - Allowing arbitrary sort columns from the URL
Why it matters
Classic SQLi reduces to one rule: parameterise every query, including the integers. Add least-privilege roles, generic error messages, statement timeouts, and a SOC detection rule on syntax-error spikes. With those four in place, in-band SQLi stops being a class of incident.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// In-band: result is visible in the page. // Symptom: long error messages, password-less logins, weird ORDER BY. // Fix: parameters, ORM, allow-list for identifiers.Try it Yourself »
Discussion
Loading…