SQL Injection Prevention Across Languages and Frameworks

Type: Software Reference Confidence: 0.95 Sources: 7 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

#VulnerabilityRiskVulnerable CodeSecure Code
1Classic injection (tautology)Critical"SELECT * FROM users WHERE name='" + input + "'""SELECT * FROM users WHERE name = ?" with params
2UNION-based injectionCritical"SELECT id FROM items WHERE cat='" + cat + "'"Parameterized query + column type validation
3Blind boolean injectionHighSame as #1; attacker infers data via true/false responsesParameterized queries + generic error messages
4Time-based blind injectionHighSame as #1; attacker uses SLEEP() / pg_sleep()Parameterized queries + query timeout limits
5Second-order injectionHighStored user input later interpolated into SQLParameterize ALL queries, including those using stored data
6Stored procedure injectionHighEXEC('SELECT * FROM ' + @table)Use sp_executesql with parameters or allowlist table names
7LIKE clause injectionMedium"WHERE name LIKE '%" + search + "%'""WHERE name LIKE ?" with escaped wildcards as param
8ORDER BY injectionMedium"ORDER BY " + columnServer-side allowlist: if column in ALLOWED_COLS
9Integer injectionMedium"WHERE id = " + id (no quotes)Parameterize even numeric values — always
10Batch/stacked queriesCritical"SELECT ...; DROP TABLE users" via multi-statementDisable multi-statement execution; parameterize queries

Decision Tree

START
|-- What language/framework?
|   |-- Python?
|   |   |-- Using SQLAlchemy/Django ORM? --> Use ORM query API (avoid .raw()/.text())
|   |   |-- Using psycopg2/sqlite3?     --> Use %s or ? placeholders with params tuple
|   |   +-- Using asyncpg?              --> Use $1, $2 numbered placeholders
|   |-- Node.js?
|   |   |-- Using Prisma/Sequelize?     --> Use ORM query builders (avoid $queryRaw)
|   |   |-- Using pg (node-postgres)?   --> Use $1, $2 parameterized queries
|   |   +-- Using mysql2?              --> Use ? placeholders with params array
|   |-- Java?
|   |   |-- Using Hibernate/JPA?       --> Use named parameters (:param) or Criteria API
|   |   +-- Using JDBC directly?       --> Use PreparedStatement with ? placeholders
|   |-- Go?
|   |   |-- Using GORM/sqlx?           --> Use struct binding or named params
|   |   +-- Using database/sql?        --> Use $1 or ? placeholders with Query/Exec
|   |-- PHP?
|   |   |-- Using Laravel Eloquent?    --> Use query builder; avoid DB::raw()
|   |   +-- Using PDO directly?        --> Use prepare() + execute() with :param or ?
|   |-- C#/.NET?
|   |   |-- Using Entity Framework?    --> Use LINQ queries; avoid FromSqlRaw with interpolation
|   |   +-- Using ADO.NET?            --> Use SqlCommand with SqlParameter objects
|   +-- Other? --> Find language-specific prepared statement API; same principle applies
|
|-- Need dynamic table/column names?
|   +-- YES --> Use server-side allowlist, NEVER parameterize identifiers
|
|-- Need LIKE pattern matching?
|   +-- YES --> Parameterize the value, escape % and _ in user input
|
+-- Need ORDER BY user input?
    +-- YES --> Map user input to allowlisted column names server-side

Step-by-Step Guide

1. Identify all SQL query construction points

Audit your codebase for any location where SQL strings are built with string concatenation, interpolation, or formatting near SQL keywords. [src1]

# Search for common vulnerable patterns in Python
grep -rn "execute.*\".*+\|execute.*f\"" --include="*.py" .
# Search in JavaScript/TypeScript
grep -rn "query.*\`.*\${\|query.*\".*+" --include="*.js" --include="*.ts" .

Verify: Every result from these searches should be reviewed and remediated.

2. Replace string concatenation with parameterized queries

For each vulnerable query, refactor to use the language-appropriate parameterized query syntax. [src1] [src4]

# BEFORE (vulnerable)
cursor.execute("SELECT * FROM users WHERE email = '" + email + "'")

# AFTER (secure)
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

Verify: Refactored query should produce identical results with normal input and reject ' OR 1=1 -- payloads.

3. Handle dynamic identifiers with allowlists

Table names, column names, and ORDER BY fields cannot be parameterized. Build a strict allowlist. [src2]

ALLOWED_SORT_COLUMNS = {'name', 'created_at', 'email', 'id'}

def safe_order_by(column, direction='ASC'):
    if column not in ALLOWED_SORT_COLUMNS:
        raise ValueError(f"Invalid sort column: {column}")
    if direction.upper() not in ('ASC', 'DESC'):
        raise ValueError(f"Invalid sort direction: {direction}")
    return f"ORDER BY {column} {direction.upper()}"

Verify: Passing "name; DROP TABLE users" should raise ValueError.

4. Secure LIKE clauses

Escape wildcard characters in user input before incorporating into LIKE patterns. [src1]

import re

def safe_like_search(cursor, search_term):
    escaped = re.sub(r'([%_\\])', r'\\\1', search_term)
    cursor.execute(
        "SELECT * FROM products WHERE name LIKE %s",
        (f"%{escaped}%",)
    )

Verify: Input %admin% should search for the literal string, not match everything.

5. Configure defense-in-depth measures

Apply the principle of least privilege and enable additional safeguards. [src3] [src7]

-- Create a restricted application database user
CREATE USER app_reader WITH PASSWORD 'strong_password';
GRANT SELECT ON users, products, orders TO app_reader;
-- Do NOT grant DELETE, DROP, ALTER, or EXECUTE to application accounts

Verify: Application user should fail on DROP TABLE attempts with a permissions error.

6. Test with automated SQL injection scanners

Run sqlmap or OWASP ZAP against your application to verify no injection points remain. [src5]

# Test a specific URL parameter with sqlmap
sqlmap -u "http://localhost:8080/api/users?id=1" --batch --level=3 --risk=2

# Test with OWASP ZAP (CLI mode)
zap-cli quick-scan -s all http://localhost:8080

Verify: Both tools should report zero SQL injection vulnerabilities.

Code Examples

Python / psycopg2: Parameterized Query

# Input:  user-supplied email string
# Output: matching user record or None
import psycopg2

conn = psycopg2.connect("dbname=myapp user=app_reader")
cur = conn.cursor()

email = request.args.get('email')
cur.execute(
    "SELECT id, name, email FROM users WHERE email = %s",
    (email,)
)
user = cur.fetchone()
cur.close()
conn.close()

Python / SQLAlchemy ORM: Safe Query

# Input:  user-supplied search term
# Output: list of matching users
from sqlalchemy import select
from models import User, Session

session = Session()
search = request.args.get('q')

stmt = select(User).where(User.name.ilike(f"%{search}%"))
results = session.execute(stmt).scalars().all()

Node.js / pg: Parameterized Query

// Input:  user-supplied id from request
// Output: user record object
const { Pool } = require('pg');
const pool = new Pool();

async function getUser(userId) {
  const result = await pool.query(
    'SELECT id, name, email FROM users WHERE id = $1',
    [userId]
  );
  return result.rows[0];
}

Java / JDBC: PreparedStatement

// Input:  user-supplied username from HTTP request
// Output: matching user or null
String username = request.getParameter("username");

String sql = "SELECT id, name, email FROM users WHERE username = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
    stmt.setString(1, username);
    ResultSet rs = stmt.executeQuery();
    if (rs.next()) {
        return new User(rs.getInt("id"), rs.getString("name"));
    }
}

Go / database/sql: Parameterized Query

// Input:  user-supplied email from query string
// Output: user struct or error
func getUser(db *sql.DB, email string) (*User, error) {
    var u User
    err := db.QueryRow(
        "SELECT id, name, email FROM users WHERE email = $1",
        email,
    ).Scan(&u.ID, &u.Name, &u.Email)
    if err != nil {
        return nil, err
    }
    return &u, nil
}

PHP / PDO: Prepared Statement

// Input:  user-supplied product ID
// Output: product record array
$pdo = new PDO('mysql:host=localhost;dbname=shop', 'app_reader', $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$productId = $_GET['id'];
$stmt = $pdo->prepare('SELECT id, name, price FROM products WHERE id = :id');
$stmt->execute(['id' => $productId]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);

C# / ADO.NET: SqlParameter

// Input:  user-supplied search term
// Output: list of matching records
string search = Request.Query["q"];
string sql = "SELECT Id, Name FROM Users WHERE Name LIKE @search";

using var cmd = new SqlCommand(sql, connection);
cmd.Parameters.Add(new SqlParameter("@search", $"%{search}%"));

using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
    results.Add(new User(reader.GetInt32(0), reader.GetString(1)));
}

Anti-Patterns

Wrong: String concatenation in Python

# BAD -- direct string concatenation allows injection
email = request.args.get('email')
cursor.execute("SELECT * FROM users WHERE email = '" + email + "'")
# Input: ' OR '1'='1' --   => returns ALL users

Correct: Parameterized query in Python

# GOOD -- parameterized query separates structure from data
email = request.args.get('email')
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
# Input: ' OR '1'='1' --   => searches for literal string, finds nothing

Wrong: Stored procedure with EXEC concatenation

-- BAD -- dynamic SQL inside stored procedure
CREATE PROCEDURE GetUser @name NVARCHAR(100)
AS
  EXEC('SELECT * FROM users WHERE name = ''' + @name + '''')

Correct: Stored procedure with sp_executesql

-- GOOD -- parameterized dynamic SQL
CREATE PROCEDURE GetUser @name NVARCHAR(100)
AS
  EXEC sp_executesql
    N'SELECT * FROM users WHERE name = @n',
    N'@n NVARCHAR(100)',
    @n = @name

Wrong: ORM raw query with unsafe interpolation

// BAD -- $queryRawUnsafe with template string interpolation
const name = req.query.name;
const users = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE name = '${name}'`  // VULNERABLE
);

Correct: ORM query builder or safe tagged template

// GOOD -- Use Prisma's query builder
const users = await prisma.user.findMany({
  where: { name: req.query.name }
});
// Or safe tagged template (Prisma auto-parameterizes)
const users2 = await prisma.$queryRaw`
  SELECT * FROM users WHERE name = ${req.query.name}
`;

Wrong: Relying only on WAF or blocklist

# BAD -- blocklist approach is always bypassable
def sanitize(input):
    blocked = ['SELECT', 'DROP', 'INSERT', '--', ';']
    for word in blocked:
        input = input.replace(word, '')
    return input
# Bypass: "SeLeCt", "SEL/**/ECT", URL encoding, Unicode tricks

Correct: Parameterize first, validate second

# GOOD -- parameterization as primary defense, validation as depth
import re

def validate_email(email):
    if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
        raise ValueError("Invalid email format")
    return email

email = validate_email(request.args.get('email'))
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

Common Pitfalls

Diagnostic Commands

# Scan a URL for SQL injection with sqlmap
sqlmap -u "http://target.com/page?id=1" --batch --level=3 --risk=2

# Scan with specific parameter and POST data
sqlmap -u "http://target.com/login" --data="user=admin&pass=test" --batch

# Run OWASP ZAP active scan from CLI
zap-cli active-scan http://target.com

# Check PostgreSQL for dangerous dynamic SQL in functions
psql -c "SELECT proname, prosrc FROM pg_proc WHERE prosrc LIKE '%EXECUTE%' AND prosrc LIKE '%||%';"

# Search codebase for vulnerable patterns (Python)
grep -rn --include="*.py" "execute.*f\"|execute.*%.*%" .

# Search codebase for vulnerable patterns (JavaScript)
grep -rn --include="*.js" "query.*\`.*\${|query.*+.*req\." .

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Any application constructs SQL queries with user inputApplication uses only static SQL with no external inputNo injection risk — but parameterize anyway as practice
Building REST APIs that accept query parametersUsing a GraphQL layer with strict typing and no raw SQLGraphQL resolvers with ORM query builders
Migrating legacy code with string-concatenated SQLInteracting with NoSQL databases (MongoDB, Redis)NoSQL injection prevention techniques
Writing stored procedures that accept parametersUsing managed query builders that auto-parameterize (Prisma, Drizzle)ORM handles it — but audit for .raw() usage
Need to support dynamic column names or ORDER BYAll query structure is static (no dynamic columns/sorting)Standard parameterized queries suffice

Important Caveats

Related Units