SQL Injection (SQLi)
Also known as: SQLi, SQL injection, Database injection
A flaw in which untrusted input is concatenated into a database query, so the database parses data as part of the command; it is prevented by keeping query structure and values separate with parameterized queries.
How it works
SQL injection is a flaw in how an application builds database queries. When the code assembles a query by joining fixed text with values the user supplied, the database receives one string and cannot tell which part the developer wrote and which part came from outside. Input that should have been treated as a value can be read as part of the command itself.
The root cause is always the same: code and data share one channel. It appears in login forms, search boxes, filters, sort parameters, headers and any field that eventually reaches a query. The consequences depend on what the database account can do, from reading records it should not, to changing or deleting data, to reaching the host in the worst configurations. That is why account privilege matters as much as the code.
The fix is structural rather than a list of forbidden characters. A parameterized query, also called a prepared statement, sends the query structure to the database first and the values separately, so a value can never become part of the command. Object-relational mappers do this for you, provided you do not fall back to concatenated raw queries.
Defence is layered. Parameterization is the primary control, allow-list validation of type, length and format reduces exposure, a database account limited to the tables and operations the application needs caps the damage, and a web application firewall plus log monitoring catch attempts and misses. Filtering and escaping alone are fragile and should never be the main defence.
Walk through it
- 1Read the vulnerable query
- 2Name the root cause
- 3Read the signals, pick the fix
- Scope every affected query
- Parameterize, restrict and verify
A pre-release review flags the order-search feature. It looks up orders by a customer name typed into a search box. Read the handler as a reviewer and note how the query text is produced.
1// GET /orders/search?customer=...2router.get("/orders/search", requireSession, async (req, res) => {3 const customer = req.query.customer;4 const sql = "SELECT id, total, status FROM orders WHERE customer_name = '" + customer + "'";5 const rows = await db.query(sql);6 res.json(rows);7});Spot it
- A burst of HTTP 500 responses and malformed-query errors from one source against a data-driven endpoint.
- WAF matches for SQL metacharacters or keywords in parameters, headers or cookies that do not usually contain them.
- Database errors returned to clients, or verbose error text appearing in responses.
- Query response times or returned row counts from one source that differ sharply from normal users.
- Unusual access to system catalogs or tables the application does not normally read, seen in database audit logs.
WAF
ts=2026-10-11T10:02:11Z rule=sql-metacharacters action=log src=203.0.113.50 host=portal.contoso-parts.example path=/orders/search param=customer
ts=2026-10-11T10:02:12Z rule=sql-metacharacters action=log src=203.0.113.50 host=portal.contoso-parts.example path=/orders/search param=customerDatabase audit log
ts=2026-10-11T10:02:12Z user=portal_app event=error code=syntax_error object=orders client_app=portal
ts=2026-10-11T10:02:40Z user=portal_app event=select object=system_catalog client_app=portal note=unexpected_for_this_accountsplSplunk: sources with a burst of server errors on a data endpoint
index=app sourcetype=access path="/orders/search" status=500
| stats count by src
| where count > 20Tune the threshold to the endpoint's baseline and correlate with WAF matches before escalating.
kqlKQL: repeated WAF hits for SQL metacharacter rules
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog" and ruleGroup_s has "SQLI"
| summarize hits=count() by clientIp_s, requestUri_s
| where hits > 20Stop it
Use parameterized queries (prepared statements) everywhere
Send the query structure and the values separately so input can never be parsed as part of the command. Use the driver's bind parameters or an ORM's query builder, and ban string-built queries in code review and static analysis.
Run the application with a least-privilege database account
Give the account only the tables and operations the feature needs, no administrative rights and no access to system objects or the file system, so a missed flaw has a small blast radius.
Validate input with allow-lists and add a WAF and monitoring as backstops
Check type, length and format, and map sort or column choices to a fixed list. Keep detailed database errors out of responses. A WAF and log alerts catch attempts, but they never replace the code fix.
Bind the value instead of joining it into the query
Vulnerable
const sql = "SELECT id, total, status FROM orders WHERE customer_name = '" + customer + "'";
const rows = await db.query(sql);Hardened
const rows = await db.query(
"SELECT id, total, status FROM orders WHERE customer_name = $1",
[customer]
);The placeholder fixes the query structure; the driver sends the value separately.
PreparedStatement with a bound parameter
Vulnerable
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, total FROM orders WHERE customer_name = '" + customer + "'");Hardened
PreparedStatement ps = conn.prepareStatement("SELECT id, total FROM orders WHERE customer_name = ?");
ps.setString(1, customer);
ResultSet rs = ps.executeQuery();- Ban concatenated query building in coding standards and enforce it with static analysis in CI.
- Use parameterized queries or an ORM's safe query API for every database call, including sort and filter features.
- Connect with a least-privilege account per application; no administrative role and no access to system objects.
- Allow-list column and sort-order choices rather than passing them through from the request.
- Return generic error messages to clients and log the detail server-side only.
- Deploy a WAF and database audit logging, and alert on bursts of errors from a single source.
If it already happened
Tighten WAF rules on the affected endpoint, block the offending sources, and if exposure is likely disable the feature while the code fix ships.
Replace every string-built query on the affected path with parameterized queries, rotate the application's database credentials, and review audit logs to scope what was read or changed.
Restore any altered data from backup, run regression tests that confirm values are bound, and notify affected parties if regulated data was exposed.
Add a static-analysis rule and a review checklist item against string-built queries, restrict the database account, and keep the log detections above permanently.
Check yourself
1. What is the root cause of SQL injection?
2. Which control is the primary fix for SQL injection?
3. Why should the application connect with a least-privilege database account?