Development Tips

PHP Security Best Practices for 2026 (A Practical Checklist)

By Ken Key Jul 24, 2024 5 min read Updated Oct 16, 2026

PHP Security Best Practices for 2026 (A Practical Checklist)
The short answer

The core of PHP security in 2026 is simple: treat every input as hostile, use prepared statements with bound parameters instead of string concatenation, escape output, and turn off debug flags in production. Most breaches I see come from skipping these basics, not from clever attacks.

Part of the complete web development guide.

On this page

Most PHP breaches I’ve seen weren’t clever. They were a query built with string concatenation, a file upload with no extension check, or a debug flag left on in production. I’ve cleaned up enough of these over the years to know the fixes are boring and repeatable, which is good news. Here’s what actually matters in 2026, with code you can drop into a real app.

PHP Security Best Practices for 2026 (A Practical Checklist)

Assume every input is hostile

This is the whole game. GET params, POST bodies, cookies, headers, uploaded filenames, even data you pulled from your own database last week. If it crossed a trust boundary, treat it as untrusted. The two mistakes that cause the most damage are SQL injection and cross-site scripting, and both come from trusting input.

Stop building queries by hand

Use prepared statements with bound parameters. Not escaping, not addslashes, not a homegrown sanitizer. PDO and mysqli both support this and there’s no excuse in 2026.

// Wrong: never do this
$sql = "SELECT * FROM users WHERE email = '$email'";

// Right: PDO with a bound parameter
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', $user, $pass, [
 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 PDO::ATTR_EMULATE_PREPARES => false,
]);

$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

One detail people miss: set PDO::ATTR_EMULATE_PREPARES to false. With emulation on, PDO does its own string interpolation on the client side, which weakens the guarantee. Turn it off so the database does the real prepared statement.

Escape on output, not input

A common bad habit is escaping everything on the way in. Don’t. Store the raw value, escape it when you render it, and escape it for the right context. For HTML bodies, htmlspecialchars() with the right flags:

echo htmlspecialchars($userComment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

For values going into a JavaScript block, a URL, or an HTML attribute, HTML escaping is the wrong tool. Use json_encode() with the appropriate flags for JS, rawurlencode() for URL components, and quote every attribute. If you’re using a template engine like Twig or Blade, its autoescaping handles the common cases, but you still have to be careful with anything you mark as raw.

PHP Security Best Practices for 2026 (A Practical Checklist): Passwords: use the built-in functions

Passwords: use the built-in functions

Do not write your own hashing. Do not use md5 or sha1 or even a plain sha256. PHP ships with password_hash() and password_verify(), and they handle salting and the algorithm for you.

$hash = password_hash($password, PASSWORD_DEFAULT);

if (password_verify($submitted, $storedHash)) {
 // Optionally rehash if the default algorithm changed
 if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
 $newHash = password_hash($password, PASSWORD_DEFAULT);
 // save $newHash
 }
}

PASSWORD_DEFAULT currently maps to bcrypt, and it may point at something newer in future PHP versions. That’s the point: use the constant and let password_needs_rehash() upgrade stored hashes as users log in. And enforce a real minimum length (12+ characters) rather than weird composition rules that push people toward Password1!.

Sessions are a common soft spot

Session hijacking and fixation are still alive in 2026, mostly because defaults are permissive. Harden the cookie settings and rotate the ID on login.

session_set_cookie_params([
 'lifetime' => 0,
 'path' => '/',
 'domain' => 'example.com',
 'secure' => true, // HTTPS only
 'httponly' => true, // no JS access
 'samesite' => 'Lax', // or 'Strict' if it fits your flow
]);
session_start();

// After a successful login:
session_regenerate_id(true);

Store session data server-side (files are fine, Redis is better at scale) and don’t stuff sensitive data in the cookie. If you’re on a shared host, make sure the session save path isn’t world-readable.

Set security headers

Headers are cheap and they close off entire classes of attack. A sensible baseline:

header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header("Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self'");

The Content-Security-Policy line is the one that stops most XSS from doing real damage, but it’s also the one most likely to break your site if you have inline scripts or third-party widgets. Start in report-only mode, watch the violations, then enforce. If you use a CDN or reverse proxy, set these there too so nothing slips through before PHP runs.

File uploads: the classic footgun

If your app accepts uploads, attackers will try to get a PHP file onto your server and then request it. Defend in layers:

  • Validate the MIME type with finfo_file(), not the browser-supplied $_FILES['type'].
  • Check the extension against an explicit allowlist.
  • Store uploads outside the web root, or in a directory where PHP execution is disabled.
  • Generate your own filename. Never trust the one the client sent.
  • Cap file size and, if you process images, re-encode them rather than trusting the original.
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['upload']['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];

if (!isset($allowed[$mime])) {
 throw new RuntimeException('Unsupported file type.');
}

$name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
move_uploaded_file($_FILES['upload']['tmp_name'], __DIR__ . '/../storage/' . $name);

Config and environment hygiene

A lot of real-world compromises come down to sloppy configuration, not clever exploits.

  • Turn display_errors off in production and log errors to a file instead. Stack traces leak paths and credentials.
  • Keep secrets in environment variables or a secrets manager, never in a file that could end up in your repo.
  • Set expose_php = Off so you stop advertising your PHP version in headers.
  • Run composer audit on a schedule. Outdated dependencies are one of the most common ways sites get hit.
  • Keep PHP itself patched. If you’re on an end-of-life version, that’s your biggest single risk.
composer audit
composer update --dry-run

If you’re maintaining a site for a business, put this on a calendar. Security isn’t a one-time task you finish; it’s a habit that keeps up with the software underneath you. When a build gets big enough that these concerns overlap with architecture, that’s usually the point where it makes sense to bring in custom web development rather than patching a plugin or a legacy file again.

Quick FAQ

Is prepared statements enough to stop SQL injection?

For the query itself, yes, as long as you bind every value and don’t concatenate user input into identifiers like table or column names. If you must build dynamic column names, validate them against a hardcoded allowlist.

Do I still need to escape output if I use a template engine?

Mostly no, because autoescaping handles HTML context. You still need to be careful when you output into JavaScript, URLs, or attributes, and whenever you bypass escaping deliberately.

What’s the single highest-value fix for an old PHP site?

Upgrade to a supported PHP version and run composer audit. Old runtimes and stale dependencies account for a huge share of real-world break-ins, and fixing them is usually faster than people expect.

Have a project like this?

I do custom web development for Long Island businesses. If you need this done right, let’s talk.