Back to Learning Center
criticalOWASP A03:2021CWE-89

SQL Injection

SQL injection (SQLi) is one of the oldest and most devastating web security vulnerabilities. It occurs when an attacker can insert malicious SQL code into queries that your application sends to its database. A successful attack can read, modify, or delete any data in your database.

Despite being well-understood since the late 1990s, SQL injection remains in the OWASP Top 10 and was detected in 94% of applications tested in recent studies. The fix is simple—parameterized queries—yet developers continue to write vulnerable code.

How SQL Injection Works

SQL injection exploits the way applications build database queries. When user input is concatenated directly into SQL strings, attackers can escape the intended query context and inject their own SQL commands.

Vulnerable Code Example

vulnerable-login.js
// VULNERABLE: User input directly in SQL string
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // This is the vulnerability - string concatenation
  const query = `
    SELECT * FROM users 
    WHERE username = '${username}' 
    AND password = '${password}'
  `;
  
  const user = await db.query(query);
  
  if (user.rows.length > 0) {
    res.json({ success: true, user: user.rows[0] });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

The Attack

An attacker enters a specially crafted username that changes the query logic:

attack-payload.txt
Username: admin' --
Password: anything

Resulting query:
SELECT * FROM users 
WHERE username = 'admin' --' 
AND password = 'anything'

The -- comments out the password check!
Attacker logs in as admin without knowing the password.

Types of SQL Injection

UNION-Based Injection

Uses UNION to combine results from another table, extracting data that shouldn't be accessible:

union-attack.sql
-- Original query for product search
SELECT name, price FROM products WHERE id = 1

-- Attacker input: 1 UNION SELECT username, password FROM users --
SELECT name, price FROM products WHERE id = 1 
UNION SELECT username, password FROM users --

-- Returns all usernames and passwords!

Blind SQL Injection

When error messages are hidden, attackers infer information through true/false responses or time delays:

blind-injection.sql
-- Boolean-based: Does the first character of admin password = 'a'?
1 AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin') = 'a'

-- Time-based: If true, sleep for 5 seconds
1; IF (SELECT COUNT(*) FROM users WHERE username='admin') > 0 
   WAITFOR DELAY '0:0:5' --

-- Attacker measures response time to extract data bit by bit

Prevention: Parameterized Queries

The only reliable defense is parameterized queries (prepared statements). They separate SQL code from data, making injection impossible.

Secure Code Examples

secure-node.js
// Node.js with pg (PostgreSQL)
const query = 'SELECT * FROM users WHERE username = $1 AND password = $2';
const result = await db.query(query, [username, password]);

// Node.js with mysql2
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password = ?',
  [username, password]
);
secure-prisma.ts
// Prisma ORM - automatically parameterized
const user = await prisma.user.findFirst({
  where: {
    username: username,
    password: hashedPassword
  }
});

// Raw queries in Prisma - still safe with template
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE username = ${username}
`;

Security Checklist

  • Use parameterized queries for ALL database operations—no exceptions
  • Use an ORM (Prisma, SQLAlchemy) that handles parameterization automatically
  • Apply least-privilege database permissions
  • Validate and sanitize all user input
  • Never trust stored data—parameterize even when using data from your own database

Practice Challenges

View all

Related Articles

View all