SQL injection (SQLi) has stayed on the OWASP Top 10 for 25 years and counting. The moment you concatenate user input into a SQL string instead of parameterising it, an attacker can rewrite your query logic: dump tables, create users, even execute OS commands. This post explains how SQLi works and how to eliminate it permanently.

Classic SQL Injection

Related guides: How to get SSL certificate · OWASP Top 10 2026 · JWT security · Password hashing guide · DDoS protection

// Vulnerable code
const email = req.body.email;  // User input
const sql = `SELECT * FROM users WHERE email = '${email}' AND password = '${req.body.password}'`;
db.query(sql);

// Attacker submits this email:
// admin@x.com' --
// The query becomes:
// SELECT * FROM users WHERE email = 'admin@x.com' --' AND password = '...'
// Everything after -- is a comment. Password check bypassed!

Blind SQL Injection

Even when the app hides query output, an attacker can extract data from the true/false behaviour of the query. For instance, by distinguishing a login form's "wrong password" response from "user not found", the attacker guesses the password hash one character at a time.

-- Boolean-based blind SQLi
admin@x.com' AND SUBSTRING(password,1,1)='a' --
-- Response: "Wrong password" means the first character is not 'a'
-- Try 'b', 'c', ... and recover one character every ~30 requests

-- Time-based blind SQLi
admin@x.com' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --
-- If the response takes 5 seconds, the first character is 'a'

Union-Based

-- Original query: SELECT name, email FROM users WHERE id = ?
-- Attacker sends as id:
1 UNION SELECT username, password FROM admins --
-- Query becomes: SELECT name, email FROM users WHERE id = 1
--                UNION SELECT username, password FROM admins --
-- Output leaks the admins table

The One Fix: Parameterized Queries

Escape functions, blacklists and character replacement can all be bypassed. The only sound approach is prepared statements / parameterized queries, which separate the query structure from the data. Data is never interpreted as SQL.

// Node.js + pg
await pool.query(
    'SELECT * FROM users WHERE email = $1 AND password_hash = $2',
    [email, passwordHash]
);

// Node.js + mysql2
await db.execute(
    'SELECT * FROM users WHERE email = ? AND password_hash = ?',
    [email, passwordHash]
);
# Python + psycopg
cur.execute(
    'SELECT * FROM users WHERE email = %s AND password_hash = %s',
    (email, password_hash)
)

# Python + SQLAlchemy (ORM)
User.query.filter_by(email=email).first()
// PHP + PDO
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();

If You Use an ORM...

Sequelize, Prisma, TypeORM, SQLAlchemy and Django ORM parameterize automatically. But watch out for raw-SQL escape hatches like raw(), literal() and query() — forget the parameters and you are vulnerable.

// Prisma — safe
await prisma.user.findMany({ where: { email } });

// Prisma raw — WRONG
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);

// Prisma raw — RIGHT
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
// The template literal tag parameterizes automatically

Layered Defence

  • Parameterized queries — the primary defence
  • Least privilege — the app DB user touches only required tables and cannot DROP/ALTER
  • Input validation — type and length checks (e.g. ids must be numeric)
  • WAF — Cloudflare or ModSecurity catches known attack patterns
  • Error handling — never surface DB errors to the user; log them instead
Warning
stored procedure alone is not enough. Procedures can also concatenate strings internally. Always call them with parameters.

Testing

# Quick test with sqlmap (on systems you own)
sqlmap -u 'https://example.com/api/user?id=1' --batch --level=3

# Automated scans with OWASP ZAP
# Manual testing in Burp Suite
# SAST with semgrep in GitHub Actions
semgrep --config=p/sql-injection .

Web Security and Application Defense

Modern web security uses defense-in-depth: TLS 1.3 and HSTS for encrypted transport, WAF (Web Application Firewall) against OWASP Top 10, BCrypt or Argon2id for password hashing, JWT tokens with proper signature verification (HMAC or RSA), CSRF tokens with SameSite cookies and Content Security Policy to mitigate XSS. Prepared statements prevent SQL injection, fail2ban or rate limiting blocks brute force, and DDoS protection via Cloudflare or anti-DDoS providers is essential. Vulnerability scanning (Burp Suite, OWASP ZAP) and regular security audits significantly reduce data leak and account takeover risks in production.

Conclusion

The SQL injection fix has not changed for 25 years: use parameterized queries. Every framework ships them by default; the only rule is never concatenate strings into SQL. A single line of discipline eliminates the class of bugs that has breached countless companies.

SQL injection security review

Codebase scan, penetration test report and developer training Contact us

WhatsApp