Safeguard
Security

What Is Literal SQL Statement Injection, and How Do You Stop It?

Literal SQL statement injection happens when user input is concatenated straight into a query string. Here is how the attack works and how to shut it down.

Marcus Chen
DevSecOps Engineer
6 min read

Literal SQL statement injection is the classic form of SQL injection: an application builds a query by pasting user input directly into a literal SQL string, so an attacker can change what the query does instead of just what it returns. If a login form takes a username and drops it verbatim into "SELECT * FROM users WHERE name = '" + input + "'", the input is no longer data. It is code. That single design choice is behind a large share of real-world data breaches, and it is entirely preventable.

This post explains the mechanism conceptually, shows what safe code looks like, and covers how to find the pattern in an existing codebase. There are no working payloads against real systems here, just enough to understand and defend against the class.

Why string concatenation is the root cause

A database engine cannot tell the difference between the SQL a developer intended and the SQL an attacker smuggled in, because by the time the query reaches the parser it is one flat string. When you concatenate, the boundary between the query template and the user's data disappears.

Consider this vulnerable Java snippet:

String sql = "SELECT id, email FROM accounts WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

If username contains a single quote, the attacker closes the string literal early and appends their own clause. The engine happily parses the result as valid SQL. Depending on the query and permissions, this can leak other rows, bypass authentication, modify data, or in some configurations reach the file system or operating system.

The important insight is that escaping quotes by hand does not solve this. Different databases have different escaping rules, comment syntaxes, and encodings, and hand-rolled escaping misses cases constantly. The fix is structural, not cosmetic.

Parameterized queries: the actual fix

The defense is to stop building SQL as a literal string. Use parameterized queries (also called prepared statements), where the query text and the parameter values travel to the database separately. The engine compiles the query template first, then binds the values as pure data that can never be reinterpreted as SQL.

Java with JDBC:

String sql = "SELECT id, email FROM accounts WHERE username = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();

Python with a DB-API driver:

cur.execute(
    "SELECT id, email FROM accounts WHERE username = %s",
    (username,),
)

Node.js with a parameterized driver:

const rows = await db.query(
  "SELECT id, email FROM accounts WHERE username = $1",
  [username]
);

In every case the ?, %s, or $1 is a placeholder, not string interpolation. Notice what is missing: no +, no template literals, no f-strings around the values. That absence is the whole point.

Where ORMs help and where they do not

Object-relational mappers like Hibernate, SQLAlchemy, Prisma, and Entity Framework parameterize by default, which is why teams using them well see far fewer injection bugs. But ORMs offer raw-query escape hatches, and those escape hatches are exactly where injection creeps back in.

A call like session.query(...).filter(text(f"name = '{name}'")) reintroduces the literal-concatenation problem inside an otherwise safe framework. So does building a WHERE clause with string formatting before handing it to a raw executor. When you review an ORM-based codebase, the raw-SQL methods are where you spend your attention.

Defense in depth

Parameterization is the primary control, but layer more around it:

  • Least privilege. The database account your app uses should only be able to do what the app needs. A read path does not need DROP or DELETE. This shrinks the damage if something does slip through.
  • Input validation. Validate type, length, and format at the edge. This is not a substitute for parameterization, but it rejects obviously malformed input early.
  • Allowlisting for dynamic identifiers. Placeholders bind values, not table or column names. If a query needs a dynamic column or sort direction, you cannot parameterize it, so match the input against a fixed allowlist of permitted identifiers instead of interpolating.
  • Stored-procedure discipline. Stored procedures are not automatically safe. A procedure that builds and executes dynamic SQL from its arguments has the same vulnerability inside the database.

Finding it in an existing codebase

Grep is a blunt but useful first pass. Search for query execution calls next to string concatenation or interpolation: executeQuery( with a + on the same line, .raw( with a template literal, f-strings feeding cursor.execute. That surfaces obvious cases fast.

For anything beyond a small repo, static analysis is the right tool. A SAST scanner performs taint analysis, tracing data from an untrusted source (an HTTP parameter) to a dangerous sink (a query executor) and flagging the flow even when it crosses several functions. That data-flow view catches the cases grep misses because the concatenation and the execution live in different files. If you want to understand how static and dynamic testing complement each other for injection, the Academy covers both, and DAST exercises running endpoints with malformed input to confirm a finding.

Injection consistently appears near the top of the OWASP Top 10 not because it is exotic, but because it is easy to write by accident and cheap to prevent once you make parameterized queries the default.

FAQ

Is escaping user input enough to prevent SQL injection?

No. Manual escaping is fragile because escaping rules differ by database, encoding, and context, and it is easy to miss a case. Parameterized queries are the reliable fix because they keep query structure and data physically separate.

Are stored procedures immune to SQL injection?

Not inherently. A stored procedure that constructs dynamic SQL from its parameters and executes it is just as vulnerable. Stored procedures that use static SQL with bound parameters are safe.

Can an ORM still be vulnerable?

Yes, through its raw-query features. When you build SQL as a string and pass it to a raw execution method, you bypass the ORM's parameterization. Keep an eye on those escape hatches during code review.

Does input validation replace parameterized queries?

No. Validation is a helpful additional layer that rejects malformed input, but it cannot anticipate every bypass. Parameterization is the primary control; validation supports it.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.