Featured image of post SQL Injection Prevention: Modern Database Security Guide Featured image of post SQL Injection Prevention: Modern Database Security Guide

SQL Injection Prevention: Modern Database Security Guide

Comprehensive guide to SQL injection prevention covering parameterized queries, ORM protection, stored procedures, WAF bypass, NoSQL variants, and automated scanning.

SQL injection remains in the OWASP Top 10 despite decades of awareness. The 2023-2024 period saw high-profile breaches in healthcare, e-commerce, and government sectors involving SQLi. While the classic ' OR 1=1 -- attack is well-known, modern variants include second-order injection, blind SQLi (time-based and boolean-based), and out-of-band exfiltration. Prevention is well-understood but poorly executed due to legacy code, ORM misuse, and insufficient testing automation.

Parameterized Queries and Prepared Statements

Prepared statements are the gold standard for SQL injection prevention. They separate SQL logic from data at the database engine level, making it impossible for user input to alter query structure.

// Node.js with pg — SAFE
const result = await client.query(
  "SELECT * FROM users WHERE email = $1 AND status = $2",
  [userEmail, "active"]
);

// Python with psycopg2 — SAFE
cursor.execute(
  "INSERT INTO orders (user_id, amount) VALUES (%s, %s)",
  [user_id, amount]
);

String interpolation is never safe, even with escaped input. Parameterized queries do not harm query plan caching — most databases cache plans for parameterized queries more effectively than for ad-hoc SQL.

LanguageSafe APIUnsafe Pattern
Node.js (pg)client.query(sql, params)String interpolation with $
Python (psycopg2)cursor.execute(sql, params)f-strings or % formatting
Java (JDBC)PreparedStatementStatement with string concat
C# (SqlClient)SqlCommand with ParametersSqlCommand with inline SQL
PHP (PDO)PDO::prepare() + execute()mysqli::query() with concatenation

Edge cases like dynamic table names or IN clauses with variable-length lists require careful handling. Use allowlisted identifiers for table names and ANY array syntax for dynamic lists.


ORM Protection and Pitfalls

ORMs like Prisma, TypeORM, Sequelize, Django ORM, and Hibernate typically protect against injection by default. However, raw queries, LIKE clauses, and ORDER BY clauses reintroduce risk.

// Prisma — raw query pitfall
const users = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE name LIKE '%${searchTerm}%'` // VULNERABLE
);

// Safe alternative with parameter binding
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE name LIKE ${"%" + searchTerm + "%"}
`;

ORDER BY cannot use parameterized columns. Always validate the column name against an allowlist:

const allowedColumns = ["name", "email", "created_at"];
if (!allowedColumns.includes(sortBy)) {
  throw new Error("Invalid sort column");
}
const result = await client.query(
  `SELECT * FROM users ORDER BY ${sortBy} ${sortDir}`,
);

ORM is not a silver bullet — developers must understand the underlying SQL behavior.


Stored Procedures and Database Hardening

Well-written stored procedures with parameters are safe. However, dynamic SQL inside a procedure using EXEC or EXECUTE IMMEDIATE reintroduces injection risk.

-- SAFE
CREATE PROCEDURE get_user(IN user_id INT)
BEGIN
  SELECT * FROM users WHERE id = user_id;
END;

-- VULNERABLE
CREATE PROCEDURE get_user(IN table_name VARCHAR(64))
BEGIN
  SET @sql = CONCAT('SELECT * FROM ', table_name);
  PREPARE stmt FROM @sql;  -- Dynamic SQL inside procedure
  EXECUTE stmt;
END;

Apply least-privilege principles: application database users should only have the permissions they need — no DROP, ALTER, or CREATE. Use PostgreSQL row-level security, SQL Server EXECUTE AS, or MySQL SQL SECURITY definer/invoker modes to enforce access at the database layer.


Input Validation and WAF as Defense-in-Depth

Input validation is insufficient alone but essential as a second layer. Prefer whitelist-based validation over blacklist filtering. Type coercion (parsing integers with parseInt), length limits, and Unicode normalization (NFC/NFD) prevent many bypass attempts.

Web Application Firewalls are a delay tactic, not a solution. Attackers bypass WAFs using comment obfuscation (/**/), encoding variations (URL, Unicode, hex, double URL encoding), HTTP parameter pollution, and less-common SQL syntax like BENCHMARK() instead of SLEEP().

-- WAF bypass: comment obfuscation
' UNION/**/SELECT/**/password/**/FROM/**/users--

-- WAF bypass: alternative functions
1 AND BENCHMARK(5000000, MD5('test'))  -- time-based blind

NoSQL Injection and Automated Scanning

NoSQL databases are also vulnerable. MongoDB $where clause injection, $regex injection for blind extraction, and JSON query object manipulation all pose risks. The principle is universal: any database language that concatenates user input is vulnerable.

DatabaseInjection VectorDefense
MongoDB$where, $regex, $neUse $eq, sanitize operators
CouchbaseN1QL string concatParameterized N1QL
CassandraCQL string concatPrepared statements
RedisEVAL/EVALSHA with LuaParameterized EVAL

Shift left with automated scanning. SAST tools like Semgrep, ESLint security plugins, and CodeQL detect injection patterns in pull requests. DAST tools like OWASP ZAP run dynamic scans against running applications.

# GitHub Actions workflow
name: Security Scan
on: pull_request
jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npx @neutralinojs/semgrep --config=auto
  dast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker run --network host owasp/zap2docker-stable zap-baseline.py -t http://localhost:3000

If prevention fails, detect injection through anomalous query patterns, unexpected errors, and outbound data volume spikes. Rotate credentials immediately, identify the injection point via query log analysis, contain the breach, and preserve forensic evidence.

SQL injection is preventable with consistent application of well-established patterns. Parameterized queries are the primary defense, ORM with raw query awareness provides structural protection, input validation and WAF add secondary layers, and automated scanning in CI/CD verifies compliance. Apply these layers consistently and you eliminate an entire class of vulnerabilities.