Module 9: Performance, Optimization, and Best Practices ⏱ 18 min

Security and SQL Injection Prevention

By the end of this lesson you will be able to:
  • Explain how SQL injection lets an attacker run unintended SQL
  • Describe parameterized queries as the primary defense
  • Recognize vulnerable string-concatenation patterns in application code

SQL injection has leaked passwords, drained accounts, and brought down companies for three decades, and it remains near the top of every security list because it is so easy to write by accident. The bug appears the moment an application glues user input directly into a SQL string instead of treating it as data.

The danger is not that the user types something rude — it is that the input is parsed as code. A carefully placed quote can close the string the app intended and append a whole new clause the developer never wrote. The database cannot tell the developer's SQL from the attacker's, so it runs both. Understanding the mechanism is the first step to never writing it, and the defense is simpler than the attack.

flowchart LR
  I["attacker submits a password"] --> C["app pastes it into the SQL string"]
  C --> Q["finished string: password = '' OR '1'='1"]
  Q --> DB["the OR condition is always true"]
  DB --> R["every row matches — login bypassed"]
  style I fill:#b45309,color:#fff
  style R fill:#b45309,color:#fff
Injection in one line: the app pastes user input into the string, and a tautology makes every row match.

Why a quote is a weapon

Everything turns on the difference between code and data. When the application writes password = ' and then drops in the user's input and a closing quote, it has handed the database a finished string to parse. If the input contains a quote of its own, that quote terminates the string early — and whatever follows is parsed as fresh SQL.

Type the password ' OR '1'='1 and the finished clause reads password = '' OR '1'='1. The database sees an empty password OR a condition that is always true, so every row matches and the login succeeds with no correct password at all. The attacker never needed a password; they needed one quote and a tautology, and the application handed them the keyboard.

A self-contained login table. The last query is the attacker's input pasted in — it returns every user.
CREATE TABLE users (
    id       INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    password TEXT NOT NULL
);

INSERT INTO users (username, password) VALUES
    ('admin', 's3cret-pw'),
    ('ada',   'ada-pw');

-- A correct password check returns one row:
SELECT username FROM users WHERE username = 'admin' AND password = 's3cret-pw';

-- The attacker submits this password:   ' OR '1'='1
-- Pasted into the string, it becomes a tautology:
SELECT username FROM users WHERE username = 'admin' AND password = '' OR '1'='1';

The defense: parameters, not strings

The reliable fix is to never build SQL by pasting strings together. A parameterized query (also called a prepared statement) sends the query skeleton and the user's values to the database in two separate pieces. The skeleton has placeholders where the values go, and the database binds each value as pure data — it is never parsed as SQL, so a quote in the input is just a quote character inside a string, not a string terminator.

Because the code and the data travel separately, there is no moment for the input to break out and become a command. The same query runs regardless of what the user typed. This is why parameterization is the single recommended defense across every major database and language — it removes the vulnerability at the root rather than chasing each tricky input.

flowchart TD
  subgraph VUL["Vulnerable: one concatenated string"]
    S1["query text + user input"] --> P1["parsed together as SQL"]
  end
  subgraph SAF["Parameterized: two channels"]
    S2["query skeleton with ?"] --> B["binder"]
    D2["user values as data"] --> B
  end
  style VUL fill:#b45309,color:#fff
  style SAF fill:#0e7490,color:#fff
Vulnerable code parses query text and input together; parameterized code sends them through separate channels.

Vulnerable vs safe, side by side

The vulnerable app builds one string by gluing the user's typing between quote characters, so the input is read as SQL. The safe app keeps the query text fixed with a placeholder and sends the user's value through a separate binding channel, where a quote is just a character inside a value.

In pseudo-code the two shapes look like this:

VULNERABLE   q = 'SELECT name FROM users WHERE id = ' + input
SAFE         q = run('SELECT name FROM users WHERE id = ?', input)

The ? is a slot the database fills with the bound value. Whatever the input contains — quotes, semicolons, a whole second statement — is matched literally against the column and can never become part of the command.

In real code this id lookup is always parameterized; whatever id is bound can only match a value.
SELECT name, role
FROM employees
WHERE id = 3;
flowchart TD
  P["Parameterized queries"] --> L1["primary defense — code and data stay separate"]
  A["Least-privilege DB account"] --> L2["limits what a successful bug can touch"]
  W["Allowlist for dynamic names"] --> L3["guards the rare structural value"]
  style L1 fill:#0e7490,color:#fff
Defense in depth: parameters are primary, but least-privilege accounts and allowlists limit the blast radius of any bug that slips through.
Severity: an injected UNION can read from a table the search never intended to touch.
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
CREATE TABLE secrets (id INTEGER PRIMARY KEY, token TEXT);
INSERT INTO products (name, price) VALUES ('Widget', 500), ('Gadget', 900);
INSERT INTO secrets (token) VALUES ('super-secret-api-key');

-- A normal product search:
SELECT name, price FROM products WHERE name = 'Widget';

-- Attacker input:   noname' UNION SELECT token, id FROM secrets --
-- The injected UNION pulls rows from a different table:
SELECT name, price FROM products WHERE name = 'noname' UNION SELECT token, id FROM secrets;
Exercise

What does SQL injection let an attacker do?

Exercise

Which technique reliably prevents SQL injection?

Exercise

A lookup by primary key is the canonical query an app should always parameterize. Write the SQL (as it reads with the value filled in) that returns the name and role of the employee whose id is 3.

SELECT
-- name and role for the employee with id 3
FROM employees
WHERE
;
Exercise

Which snippet is vulnerable to SQL injection?

Exercise

An app lets staff search employees by role and must run the query parameterized. Write the SQL (value filled in) that returns only the name of every employee whose role is 'Researcher'.

SELECT
-- names of Researchers
FROM employees
WHERE
;

Recap

  • SQL injection happens when user input is concatenated into a query string and parsed as code, not data.
  • A single quote can terminate the intended string and append a clause like OR '1'='1 that is always true.
  • Parameterized queries send the skeleton and the values separately, so input can only ever be data — the root-cause fix.
  • Manual escaping, hiding errors, and length limits do not reliably prevent injection.
  • A lookup by id or by an exact value is exactly the query that must always be parameterized in real application code.

That closes the module: you can now read query plans, choose indexes, rewrite anti-patterns, design a sound schema, debug systematically, and write SQL that an attacker cannot turn against you.

Checkpoint quiz

Why is user input dangerous when concatenated into a SQL string?

How does a parameterized query stop injection?

Go deeper — technical resources