Safety

SQL Injection Prevention

Parameterised queries, prepared statements, and why string interpolation in SQL is never acceptable.

Overview

SQL injection occurs when user-controlled data is interpolated into a SQL query string, allowing attackers to alter the query's logic. It consistently ranks #1 in OWASP Top 10 and enables data exfiltration, authentication bypass, data modification, and sometimes OS command execution. Prevention is through parameterised queries or prepared statements, which separate SQL code from data.

Origin

The term "SQL injection" was popularised by Jeff Forristal (Rain Forest Puppy) in Phrack magazine (1998). High-profile incidents include the 2008 Heartland Payment Systems breach (134 million cards) and the 2012 LinkedIn hack, both attributed to SQL injection. OWASP has listed it in every edition of the Top 10 since 2003.

Examples

Parameterised queries in TypeScript with pg

import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// VULNERABLE: string interpolation
async function findUserVulnerable(email: string) {
  // If email = "' OR '1'='1" this returns all users
  const result = await pool.query(
    "SELECT * FROM users WHERE email = '" + email + "'"
  );
  return result.rows;
}

// SAFE: parameterised query - $1 is a placeholder, not string interpolation
async function findUser(email: string) {
  const result = await pool.query(
    'SELECT id, email, name FROM users WHERE email = $1 AND active = true',
    [email]
  );
  return result.rows[0] ?? null;
}

// SAFE: multiple parameters
async function searchOrders(customerId: string, status: string, limit: number) {
  const result = await pool.query(
    'SELECT id, total, status FROM orders WHERE customer_id = $1 AND status = $2 LIMIT $3',
    [customerId, status, limit]
  );
  return result.rows;
}

Parameterised queries send the SQL and data in separate protocol messages; the database parses the SQL structure before seeing the data, making injection structurally impossible. The $1, $2 placeholders are PostgreSQL syntax; MySQL uses ? positional placeholders.

Safe dynamic queries with Arel in Ruby on Rails

class OrderQuery
  def initialize(scope = Order.all)
    @scope = scope
  end

  # SAFE: ActiveRecord parameterisation
  def by_customer(customer_id)
    @scope = @scope.where(customer_id: customer_id)
    self
  end

  # SAFE: hash syntax prevents injection
  def with_status(status)
    allowed = %w[pending confirmed shipped cancelled]
    raise ArgumentError, "Invalid status: #{status}" unless allowed.include?(status.to_s)
    @scope = @scope.where(status: status)
    self
  end

  # SAFE: Arel for dynamic column sorting (never interpolate column names)
  def sorted_by(column, direction = 'asc')
    allowed_columns = %w[created_at total status]
    allowed_directions = %w[asc desc]
    raise ArgumentError unless allowed_columns.include?(column.to_s)
    raise ArgumentError unless allowed_directions.include?(direction.to_s)
    @scope = @scope.order(Order.arel_table[column].send(direction))
    self
  end

  def results = @scope
end

orders = OrderQuery.new
  .by_customer(params[:customer_id])
  .with_status('confirmed')
  .sorted_by('created_at', 'desc')
  .results

Column names and sort directions cannot be parameterised; they must be validated against an allowlist. ActiveRecord's hash syntax (where(status: value)) automatically parameterises the value. Never use where("status = #{value}") with string interpolation.

Use Cases

  • 01Any application that accepts user input and uses it to query a relational database
  • 02Dynamic search and filter UIs where multiple optional WHERE clauses are constructed from query parameters
  • 03Admin interfaces that build queries from form inputs (column filters, sort controls)
  • 04APIs that accept identifiers in path parameters or query strings used in database lookups

When Not to Use

  • //There is no case where SQL injection is acceptable; parameterised queries are the correct approach in every scenario
  • //Do not use an ORM as a false security guarantee; ORMs support raw SQL escape hatches (ActiveRecord.find_by_sql, Sequelize.query) that reintroduce injection if user data is interpolated
  • //Do not rely on input sanitisation (escaping quotes) as the primary defence; parameterisation is structurally safe while escaping can be bypassed with unusual character encodings

Technical Notes

  • Second-order SQL injection occurs when unsanitised data is stored and later used to construct a query; input sanitisation at write time does not protect against this, but parameterised queries at read time do
  • NoSQL databases (MongoDB, DynamoDB) have analogous injection vulnerabilities: MongoDB operator injection via $where, $regex, $gt in JSON payloads. Validate and sanitise operator keys in user-supplied filter objects
  • Stored procedures do not inherently prevent SQL injection; a stored procedure that builds dynamic SQL via EXEC or sp_executesql and interpolates parameters is still vulnerable
  • sqlmap is the standard open-source tool for automated SQL injection detection and exploitation; running it against your own application in a staging environment is a practical way to verify parameterisation coverage