cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) — parameterized query pattern (syntax varies by language)..raw(), @Query, execute()) bypass parameterization| # | Vulnerability | Risk | Vulnerable Code | Secure Code |
|---|---|---|---|---|
| 1 | Classic injection (tautology) | Critical | "SELECT * FROM users WHERE name='" + input + "'" | "SELECT * FROM users WHERE name = ?" with params |
| 2 | UNION-based injection | Critical | "SELECT id FROM items WHERE cat='" + cat + "'" | Parameterized query + column type validation |
| 3 | Blind boolean injection | High | Same as #1; attacker infers data via true/false responses | Parameterized queries + generic error messages |
| 4 | Time-based blind injection | High | Same as #1; attacker uses SLEEP() / pg_sleep() | Parameterized queries + query timeout limits |
| 5 | Second-order injection | High | Stored user input later interpolated into SQL | Parameterize ALL queries, including those using stored data |
| 6 | Stored procedure injection | High | EXEC('SELECT * FROM ' + @table) | Use sp_executesql with parameters or allowlist table names |
| 7 | LIKE clause injection | Medium | "WHERE name LIKE '%" + search + "%'" | "WHERE name LIKE ?" with escaped wildcards as param |
| 8 | ORDER BY injection | Medium | "ORDER BY " + column | Server-side allowlist: if column in ALLOWED_COLS |
| 9 | Integer injection | Medium | "WHERE id = " + id (no quotes) | Parameterize even numeric values — always |
| 10 | Batch/stacked queries | Critical | "SELECT ...; DROP TABLE users" via multi-statement | Disable multi-statement execution; parameterize queries |
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
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.
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.
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.
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.
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.
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.
# 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()
# 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()
// 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];
}
// 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"));
}
}
// 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
}
// 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);
// 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)));
}
# 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
# 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
-- BAD -- dynamic SQL inside stored procedure
CREATE PROCEDURE GetUser @name NVARCHAR(100)
AS
EXEC('SELECT * FROM users WHERE name = ''' + @name + '''')
-- 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
// BAD -- $queryRawUnsafe with template string interpolation
const name = req.query.name;
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE name = '${name}'` // VULNERABLE
);
// 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}
`;
# 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
# 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,))
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false). [src1].raw() or .execute() method that bypasses parameterization. Fix: Audit all raw query usage; parameterize within raw queries. [src2]WHERE id = ? not WHERE id = + id. [src3]% or _ changes LIKE query behavior. Fix: Escape %, _, and \ in user input before adding wildcards. [src1]allowMultiQueries: false). [src4]# 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\." .
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Any application constructs SQL queries with user input | Application uses only static SQL with no external input | No injection risk — but parameterize anyway as practice |
| Building REST APIs that accept query parameters | Using a GraphQL layer with strict typing and no raw SQL | GraphQL resolvers with ORM query builders |
| Migrating legacy code with string-concatenated SQL | Interacting with NoSQL databases (MongoDB, Redis) | NoSQL injection prevention techniques |
| Writing stored procedures that accept parameters | Using managed query builders that auto-parameterize (Prisma, Drizzle) | ORM handles it — but audit for .raw() usage |
| Need to support dynamic column names or ORDER BY | All query structure is static (no dynamic columns/sorting) | Standard parameterized queries suffice |
User.objects.filter(name=x) in Django is safe, but User.objects.raw(f"SELECT * FROM users WHERE name = '{x}'") is not. Audit every raw query method.? (SQLite, MySQL JDBC), %s (psycopg2), $1 (node-postgres, asyncpg), @param (ADO.NET), :param (cx_Oracle, Hibernate).