Skip to content

Executar uma prepared statement (fazer bind, não concatenar) snippet

Uma prepared statement envia a query e os dados como mensagens separadas — a base de dados compila o template uma vez e faz bind dos valores para sempre.

Uma prepared statement envia a query e os dados como mensagens separadas — a base de dados compila o template uma vez e faz bind dos valores para sempre. Esta é A defesa contra SQL injection: não existe uma string de onde ' OR 1=1 -- possa escapar, porque o input do utilizador nunca se torna texto da query. A armadilha é o formato da API: o marcador de bind difere em cada driver ($1, ?, :name) e a saída clássica — montar a string da query com valores concatenados 'só para este caso' — é a vulnerabilidade. Wildcards de LIKE e identificadores (nomes de tabelas) não podem ser ligados como valores; valide-os contra uma allowlist.

Receita executável · 11 linguagens
Databases & SQLsqlprepared-statementssql-injectionbind-parameterspostgresparameterized-query

Every language

11 linguagens, 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 rejected

Prepared 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.

Run in the SQL playground →
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 text

prepare/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.