Every language
11 languages, copy-ready. One at a time with syntax highlighting, or all inline.
SQLSQLrunnable
-- the template: $1 is a bind slot, not text — nothing to escape into
PREPARE get_user AS
SELECT id, name, email FROM users WHERE id = $1;
EXECUTE get_user(42); -- the data arrives as a VALUE, never as query text
DEALLOCATE get_user; -- or it lives until the session ends
-- EXECUTE get_user('42 OR 1=1'); -- ERROR: invalid input syntax —
-- a payload that reaches a bind slot stays data and is rejectedPrepared statements live in the SESSION: they die with the connection and collide by name (re-PREPARE without DEALLOCATE is an error), so run PREPARE and EXECUTE in the same batch. The Postgres rule the demo leans on: the $1 slot has the column's type, and EXECUTE casts the argument to it — an injection payload fails the cast, it cannot become query text.
JSJavaScript
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// one call, two payloads: the template rides one wire message, values another
const { rows } = await pool.query(
'SELECT id, name FROM users WHERE id = $1',
[userId],
);
// NEVER: `SELECT ... WHERE id = ${userId}` — the value became query text$1/$2 are Postgres positional markers. node-postgres sends trivial queries as text but auto-prepares non-trivial ones on the wire and CACHES server-side prepared statements by name — the same call site in a loop reuses one compiled plan. The template is the only string in the call; if a variable touches it, it is code, not data.
TSTypeScript
import type { Pool } from 'pg';
async function query<T>(
pool: Pool,
sql: string,
params: unknown[] = [],
): Promise<T[]> {
const result = await pool.query(sql, params);
return result.rows as T[];
}
interface UserRow { id: number; name: string }
const users = await query<UserRow>(
pool,
'SELECT id, name FROM users WHERE id = $1',
[userId],
);The generic makes the ROW shape the contract; the trap is cast-acting — `as T[]` trusts the template, so a template that drifts from the interface lies at runtime, not compile time. The template is the only string: values ride the params array, so userId can never change what the query does.
GoGo
import (
"context"
"database/sql"
)
func userName(ctx context.Context, db *sql.DB, id int64) (string, error) {
var name string
// database/sql prepares transparently — you never see it happen
err := db.QueryRowContext(ctx,
"SELECT name FROM users WHERE id = ?", id).Scan(&name)
return name, err
}
// hot loop? prepare ONCE, execute N — one compiled plan:
func userNames(ctx context.Context, db *sql.DB, ids []int64) ([]string, error) {
stmt, err := db.PrepareContext(ctx, "SELECT name FROM users WHERE id = ?")
if err != nil {
return nil, err
}
defer stmt.Close() // leaked stmts pin their connection until it dies
names := make([]string, 0, len(ids))
for _, id := range ids {
var name string
if err := stmt.QueryRowContext(ctx, id).Scan(&name); err != nil {
return nil, err
}
names = append(names, name)
}
return names, nil
}database/sql prepares behind the scenes and rewrites ? to the driver's marker ($1, @p1). The explicit PrepareContext is for REUSE loops — and stmt.Close() matters: a stmt holds one connection from the pool until closed. fmt.Sprintf into the template is the vulnerability; the args slice is the only ride data gets.
RsRust
use sqlx::PgPool;
async fn user_name(pool: &PgPool, id: i64) -> Result<String, sqlx::Error> {
// COMPILE-TIME checked: columns, types, and the bind are verified
// against the real schema before the binary exists
let user = sqlx::query!("SELECT name FROM users WHERE id = $1", id)
.fetch_one(pool)
.await?;
Ok(user.name)
}
// no live DB at build time? SQLX_OFFLINE=true + cargo sqlx prepare's
// checked-in .sqlx cache. Dynamic SQL (identifiers can't be a $bind)?
// build it with sqlx::query(...) — never format!() on the SQL string.query! is sqlx's superpower: a wrong column or type fails the BUILD, not production — id inside the macro is a BIND, not interpolation, and format!() on the SQL string is the vulnerability. Offline mode (SQLX_OFFLINE + the .sqlx cache) keeps CI green without a live database.
PHPPHP
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // REAL prepares on the server
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]); // ? is positional: values land in marker order
foreach ($stmt as $row) {
// $row['name'] ...
}
// the vulnerability this replaces:
// $pdo->query("SELECT ... WHERE email = '$email'"); // string-built textprepare/execute is a two-step — the template goes once, values ride execute()'s array. ATTR_EMULATE_PREPARES => false is the load-bearing line: emulated prepares quietly build the SQL string CLIENT-side, reintroducing interpolation-shaped bugs. LIKE needs the wildcard IN the value: execute(['%' . $q . '%']) — never quote() the whole template.
PyPython
import psycopg
with psycopg.connect(ENV['DATABASE_URL']) as conn:
with conn.cursor() as cur:
# %s is the DBAPI bind marker, NOT python % formatting:
cur.execute(
'SELECT id, name FROM users WHERE id = %s',
(user_id,), # a one-element tuple — the comma IS the tuple
)
row = cur.fetchone()
# identifiers (table/column names) can never be a %s — compose them safely:
from psycopg import sql
query = sql.SQL('SELECT id FROM {} WHERE id = %s').format(
sql.Identifier('users'))The classic self-inflicted injection is execute(sql % params) — the % operator does the interpolation psycopg was protecting you from. (user_id,) keeps one value a sequence; a bare string would be split into characters, one per marker. psycopg.sql composes identifiers: sql.Identifier quotes a table/column name that can never ride as a value.
C#C#
using var conn = new SqlConnection(connectionString);
conn.Open();
using var cmd = new SqlCommand(
"SELECT name FROM users WHERE id = @id", conn); // @name markers
cmd.Parameters.Add("@id", SqlDbType.BigInt).Value = userId; // explicit type
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var name = reader.GetString("name");
}
// the vulnerability this replaces:
// new SqlCommand($"SELECT name FROM users WHERE id = {userId}", conn)@name markers — and AddWithValue's type inference is the local trap: it sends a string as NVARCHAR against a VARCHAR column, which defeats plan reuse and can kill an index scan. Add + explicit SqlDbType keeps the compiled plan precise. The $ interpolation variant replaces the string entirely — it is the same bug as concatenation.
JvJava
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT name FROM users WHERE id = ?")) { // ? = positional bind slot
ps.setLong(1, userId); // 1-based index, typed setter
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String name = rs.getString("name");
}
}
}
// the anti-pattern this replaces — Statement + concatenation:
// Statement st = conn.createStatement();
// st.executeQuery("SELECT name FROM users WHERE id = " + userId);PreparedStatement binds with ? and typed setXxx setters (1-based); plain Statement with + concatenation is THE anti-pattern — userId becomes query text. ps is reusable: setXxx + execute again in a loop reuses the compiled statement, but create it OUTSIDE the loop or you re-prepare every row. Identifiers can't be a ? — validate table names against an allowlist.
KtKotlin
dataSource.connection.use { conn ->
conn.prepareStatement("SELECT name FROM users WHERE id = ?").use { ps ->
ps.setLong(1, userId) // same JDBC as Java, 1-based ?
ps.executeQuery().use { rs ->
if (rs.next()) rs.getString("name") else null
}
}
}
// Spring's named form — :name markers read from a Map:
// named.queryForObject(
// "SELECT name FROM users WHERE id = :id",
// mapOf("id" to userId), String::class.java)use{} closes on every exit — result set, statement, connection, in that order. NamedParameterJdbcTemplate swaps ? for :name and reads values from a Map, so the marker list and the args array can no longer drift apart. Same rule as Java: concatenate a value into the string and it is code, not data.
RbRuby
require 'sequel'
# Sequel: the ?-array IS the bind — placeholders and values never meet as text:
user = DB['SELECT id, name FROM users WHERE id = ?', user_id].first
# plain pg gem — params ride exec_params, NEVER exec (exec is raw text):
# conn.exec_params('SELECT id, name FROM users WHERE id = $1', [user_id])
# ActiveRecord: hash conditions are binds by construction:
# User.where(email: email).pick(:id)Sequel's ?-array (and Rails' where(hash:)) binds positionally — the driver ships values as a separate message. The pg-gem split is the trap: exec takes ONE raw string (the injection door), exec_params(sql, [values]) is the prepared form with $1 markers. Interpolating a table name with #{} is the remaining hole — Sequel identifiers (Sequel[:users]) compose those safely.