ROOTVIELROOTVIEL
← Knowledge Base

Application & code

Educational12 min

SQL Injection

An application builds a SQL statement by concatenating untrusted input into the query string, so the data crosses into the command and the attacker rewrites the query — reading, modifying, or destroying data the application never intended to expose.

Classification

Domain
Application & code
ROOTVIEL support
Educational — Recognized concept, documented for learning — not detected by ROOTVIEL.
Analysis family
code-src
CWE
CWE-89
OWASP
A03:2021-Injection

Definition

SQL injection is the failure to keep user-supplied data separate from the SQL command that uses it. The application constructs a query as a string and splices attacker-controllable input directly into that string, so input that was meant to be a value (a name, an id, a search term) is parsed by the database as part of the SQL grammar. Once the boundary between code and data is gone, the attacker can append clauses, change the logic, union in other tables, or chain additional statements.

It is the archetypal injection flaw: the defect is not in the database, which faithfully executes the SQL it is handed, but in the application code that let data become command. The same pattern recurs across every data-access layer — raw drivers, query builders used unsafely, ORMs dropped into raw-SQL mode, and NoSQL query documents assembled from strings.

Context

SQL injection appears anywhere an application turns external input into a database query and does so by assembling text rather than binding parameters. The input need not come from a form field; it can arrive from an HTTP header, a cookie, a JSON body, an uploaded file name, a webhook, or a value previously stored in the database and read back (second-order injection).

  • ▸Login, search and filter endpoints that concatenate a WHERE clause from request parameters.
  • ▸Reporting and admin tools that let a dynamic column name, sort order, or table name flow into the query.
  • ▸Legacy data layers using string-formatted queries, or ORMs used in raw/`extra` mode.
  • ▸Second-order flows: input is stored safely, then later read and concatenated into a new query without re-treating it as untrusted.
  • ▸NoSQL and query-document engines where a string or object is built from input and interpreted as an operator expression.

Technical fundamentals

A database receives a statement, parses it into a query plan, and executes it. The only safe design is to send the SQL text and the data values over separate channels: the text contains placeholders, and the values are bound to those placeholders after parsing, so a value can never be reinterpreted as syntax. This is parameterisation (prepared statements). When instead the value is concatenated into the text before parsing, the parser sees whatever the attacker wrote — a closing quote, a comment marker, a UNION, a stacked statement — and treats it as grammar.

The right mental model is a taint dataflow: external input is a taint source, the database execution call is a sink, and a vulnerability exists when tainted data reaches the sink as part of the query string with no sanitiser that removes the taint for SQL. Parameter binding is the sanitiser that works, because it does not try to clean the value — it changes the channel so the value is never parsed as code. Escaping and quoting are fragile substitutes that break on edge cases (character sets, numeric contexts, identifiers).

Parameterised query
A statement whose values are bound to placeholders after the SQL is parsed, so data cannot become syntax.
Taint source / sink
The source is untrusted input; the sink is the database execute call. Injection is a source→sink path with no effective sanitiser.
Second-order injection
Input stored today and concatenated into a query later; safe storage does not make it safe to splice into SQL on read.
Stacked queries
Multiple statements separated by a delimiter in one call, so an appended statement runs after the intended one (driver-dependent).

Root cause

The root cause is a missing separation between code and data at the moment the query is built. The developer treats the query as a template to be filled by string formatting, which fuses the value into the command. Everything else — the quote escaping, the input validation, the WAF rules — is a patch around the fact that the value and the SQL share one channel. Parameterisation removes the cause rather than mitigating the symptom, because it makes it structurally impossible for a bound value to be parsed as SQL.

A secondary cause is the belief that some positions in a query can be parameterised and some cannot (identifiers such as table and column names, or the ASC/DESC direction, are not bindable values), so developers fall back to concatenation for those positions and carry the untrusted string in with it. That is a real constraint of SQL, but the answer is an allow-list of permitted identifiers, not raw concatenation.

How the attack works, step by step

  1. 1The application builds a query by concatenating a request parameter into the SQL string, e.g. a login lookup on username and password.
  2. 2The attacker supplies a value containing SQL syntax — a quote to close the string literal, then a boolean clause such as OR 1=1, then a comment marker to swallow the rest of the query.
  3. 3The database parses the combined string: the injected clause becomes part of the WHERE condition, so the filter now matches every row.
  4. 4The attacker escalates — UNION SELECT to read other tables, subqueries to extract column values character by character (blind injection via boolean or time responses), or stacked statements to modify data where the driver allows it.
  5. 5Because data and code shared one channel, nothing distinguished the attacker’s SQL from the application’s own — the database executed both as one statement.

Blind variants work even when no data is returned to the page: the attacker infers each bit of a secret from whether the response changes (boolean-based) or how long it takes (time-based, using a sleep function), automated to exfiltrate whole tables.

Vulnerable example

A synthetic, illustrative login handler that concatenates request input into the query string. The input crosses into the command, so a crafted username rewrites the WHERE clause:

synthetic — do not use; illustrates the flaw
// UNSAFE: request input concatenated into the SQL text (data becomes code). async function login(req, db) { const user = req.body.username const pass = req.body.password // The quotes live in the template; the attacker can close them and inject. const sql = "SELECT id, role FROM users " + "WHERE username = '" + user + "' " + "AND password = '" + pass + "'" const rows = await db.query(sql) // sink: the database parses the whole string return rows[0] || null } // Attacker sends username: admin' -- // Resulting SQL: // SELECT id, role FROM users WHERE username = 'admin' --' AND password = '...' // The "--" comments out the password check; the attacker is logged in as admin.

Corrected example

The fix is parameterisation: the SQL text carries placeholders, and the values are bound separately so the database never parses them as syntax. For positions that cannot be bound (identifiers, sort direction), map the input through a code-defined allow-list rather than concatenating it.

synthetic — parameter binding plus an identifier allow-list
// SAFER: values are bound, never concatenated; identifiers are allow-listed. const SORT_COLUMNS = { name: 'name', created: 'created_at' } // allow-list const SORT_DIRS = { asc: 'ASC', desc: 'DESC' } async function login(req, db) { const sql = "SELECT id, role FROM users WHERE username = ? AND password_hash = ?" // Placeholders are filled AFTER parsing — the value cannot become SQL. const rows = await db.query(sql, [req.body.username, hash(req.body.password)]) return rows[0] || null } async function listUsers(req, db) { // Identifiers are not bindable values: resolve via an allow-list, never concat. const col = SORT_COLUMNS[req.query.sort] || 'name' const dir = SORT_DIRS[req.query.dir] || 'ASC' const sql = "SELECT id, name FROM users ORDER BY " + col + " " + dir + " LIMIT ?" return db.query(sql, [50]) }
Note

Limit of the fix: parameterisation defends bindable values completely, but identifiers, ORDER BY direction, and dynamic LIMIT/OFFSET in some drivers are not bindable — those must be resolved through an allow-list. An ORM does not make this automatic: its raw-SQL, `extra`, and string-filter escape hatches reintroduce the same flaw. Least-privilege database accounts and disabling multi-statement execution reduce blast radius but do not remove the injection.

Impact

  • ▸Confidentiality: read arbitrary tables — credentials, tokens, personal data — via UNION or blind extraction.
  • ▸Integrity: modify or delete rows, elevate a user’s role, or tamper with financial records through injected UPDATE/DELETE.
  • ▸Authentication bypass: rewrite a login WHERE clause so the check always passes.
  • ▸Availability and lateral movement: drop tables, exhaust the database, or on some engines reach the host via built-in procedures and file access.

Detection

The primary structural signal is static taint analysis: mark request input as a source, mark the database execute calls as sinks, and look for a path where tainted data is concatenated or interpolated into the query string with no parameter binding in between. A match is a candidate, not a proof — the analyser can miss dynamically assembled SQL and can over-report where an unseen sanitiser exists. Dynamic testing confirms exploitability: send payloads that provoke a SQL error, a boolean difference, or a measurable time delay, and observe whether the query logic changed. Code review that simply looks for placeholders versus string concatenation remains one of the most reliable checks.

Where ROOTVIEL fits

This entry is reference material. ROOTVIEL’s verified, execute-and-prove engine is focused on the AI/agentic and Web3 domains, where a finding is confirmed by running the attack in a sandbox and checking a deterministic invariant. SQL injection is an application/code weakness that ROOTVIEL does not currently detect with a verified engine, so a finding here is educational unless it is independently demonstrated against the specific code and database in scope.

Note

Support: educational. The concept is well established and the dataflow (input → query string → execute) is amenable to taint analysis, but ROOTVIEL does not promote a verified SQL-injection finding. Treat any pattern match as a hypothesis to confirm by hand or with a dedicated tool, never as a proven vulnerability.

Mitigation

Remove the shared channel: data and SQL must never be parsed together. Everything else is defence in depth.

  • ▸Preventive: use parameterised queries / prepared statements for every value, everywhere, with no exceptions for “trusted” inputs.
  • ▸Preventive: for non-bindable positions (table/column names, sort direction), map input through a fixed allow-list of permitted identifiers; never concatenate the raw string.
  • ▸Preventive: prefer an ORM or query builder used in its safe mode, and audit every raw-SQL / `extra` escape hatch.
  • ▸Defence in depth: run the application on a least-privilege database account, disable stacked/multi-statement execution where possible, and validate input types (an id should be an integer).
  • ▸Detective: run static taint analysis in the build and review new data-access code for concatenation into queries.

Verifying the mitigation

Verify by re-running the taint analysis and confirming no request-input→query-string path remains without parameter binding, and by inspecting that identifier positions resolve through an allow-list. Then attempt exploitation directly: send quote, comment, UNION, boolean, and time-based payloads to each previously vulnerable parameter and confirm the query logic no longer changes. State the result as "these attempts failed against the tested inputs", not as proof that no injection exists anywhere in the application.

Note

A clean static scan means no unparameterised path was found in the analysed code — dynamically built SQL the analyser could not resolve may still exist. Pair static results with targeted dynamic probing of each data-access endpoint.

Common mistakes

  • ▸Escaping quotes by hand instead of binding parameters — fragile and defeated by character-set and numeric-context edge cases.
  • ▸Parameterising most queries but concatenating "just the column name" or sort direction, reintroducing injection through identifiers.
  • ▸Assuming an ORM is automatically safe while using its raw-SQL or string-filter escape hatches.
  • ▸Trusting input because it was validated once or stored in the database earlier (missing second-order injection).
  • ▸Relying on a WAF or blocklist of keywords as the primary control instead of removing the code/data mixing.

References

  1. [1]MITRE CWE-89 (SQL Injection)
  2. [2]OWASP Top 10 — A03:2021 Injection
  3. [3]OWASP Cheat Sheet — SQL Injection Prevention
  4. [4]OWASP Web Security Testing Guide
  5. [5]PortSwigger Web Security Academy — SQL injection

References point to public, well-known technical sources for further study.

Related articles