Build the mental model
SQL injection happens when an application builds a SQL query by directly concatenating untrusted input into the query string. The database then cannot tell the difference between data the user typed and SQL syntax the developer wrote.
Never build SQL by concatenating untrusted input
This is stated bluntly because it is the single most common way applications get compromised. Treat any string like "...WHERE email = '" + userInput + "'" as a bug the moment you see it, regardless of how the input is later validated — the fix is to stop concatenating, not to sanitize harder.
A parameterized query sends the SQL structure and the input value through two separate channels. The template is fixed ahead of time, and the value is bound afterward, purely as data — never as part of the query's structure.
Every mainstream driver and ORM supports parameterized queries as its default, safe way to accept input. The SQL and PostgreSQL tutorials on this site show the concrete, language-specific syntax.
UNSAFE VS SAFE QUERY CONSTRUCTION
---------------------------------
UNSAFE: string concatenation
userInput ------------------+
v
"SELECT * FROM users WHERE email = '" + userInput + "'"
|
v
one blended string of SQL text
(input can change the SQL structure)
SAFE: parameterized query
SQL template: "SELECT * FROM users WHERE email = $1"
|
v
locked in before input arrives
userInput ------------------+
v
bound separately, as pure data
(input can never change the structure)Connect it to a real scenario
Defending against SQL injection is mostly about habit. Every place user input reaches a query should go through a parameterized-query API from the first line of code, not as a later fix.
ORMs make this nearly automatic. The risk resurfaces when a project drops to raw SQL and someone reaches for string concatenation out of familiarity — that is the exact moment worth pausing on.
The code below shows both halves defensively: a pattern-detector flagging dangerous-looking input, and a simulated parameterized query that handles the same input safely regardless of content. No exploit runs against a real database.
Never build SQL queries by concatenating untrusted input
This protects your own application. Once user input can reshape a query's structure, an attacker can potentially read, modify, or delete data far beyond what your app's own logic ever intended to expose.
Try the working example
// Defensive pattern-detector: flags characters/keywords that are dangerous
// ONLY if someone concatenates them directly into a raw SQL string.
function looksDangerousIfConcatenated(input) {
const suspiciousPattern = /('|--|;|\bor\b|\bunion\b|\bdrop\b)/i;
return suspiciousPattern.test(input);
}
// Simulated parameterized query: a real driver sends the SQL text and the
// parameter values as SEPARATE channels, so the value is always treated as
// plain data and can never change the query's structure.
function runParameterizedQuery(sqlTemplate, params) {
return { sql: sqlTemplate, boundParams: params, executedAsData: true };
}
const suspiciousInput = "' OR '1'='1";
const normalInput = "alice@example.com";
console.log("suspicious input flagged:", looksDangerousIfConcatenated(suspiciousInput));
console.log("normal input flagged:", looksDangerousIfConcatenated(normalInput));
console.log(runParameterizedQuery("SELECT * FROM users WHERE email = $1", [suspiciousInput]));
console.log(runParameterizedQuery("SELECT * FROM users WHERE email = $1", [normalInput]));The detector flags the suspicious input "' OR '1'='1" as true and the normal input "alice@example.com" as false. Both then pass through the simulated parameterized query, which returns { sql: "SELECT * FROM users WHERE email = $1", boundParams: [input], executedAsData: true } for either one — the safe path treats both identically, regardless of what the flagged input contains.5-minute try-it
Add a third test input, "O'Brien" (a legitimate last name containing an apostrophe), and run the code. Does the detector flag it? Explain why parameterized queries still handle it correctly even if the detector's pattern-matching is imperfect.
One important caution
Relying on input sanitization or escaping as the primary defense instead of switching to parameterized queries, which removes the entire bug category by construction.
Assuming an ORM protects every query automatically, then dropping into raw, concatenated SQL for one 'just this once' complex query.
OWASP: SQL Injection Prevention Cheat Sheet — How Databases Work