System Design Simple
Security

PHP security: 7 common vulnerabilities and how to fix them

Nov 1, 2026 · updated Nov 1, 2026 · 8 min read

Short answer

Most PHP vulnerabilities come from seven mistakes: SQL built from user input, unescaped output (XSS), missing CSRF tokens, weak password hashing, unsafe uploads, session fixation and unserialize on untrusted data. The fixes are prepared statements, htmlspecialchars, random tokens, password_hash, finfo checks, session_regenerate_id and JSON.

Key facts
SQL injectionPDO prepared statements
XSShtmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE, UTF-8) + CSP
CSRFrandom_bytes token + hash_equals
Passwordspassword_hash / password_verify
Uploadsfinfo type check, random name, outside web root
Sessionssession_regenerate_id(true), Secure, HttpOnly, SameSite

Most PHP security problems (脆弱性) come from the same handful of mistakes. The good news: each one has a simple, well-known fix. This is a defensive checklist you can apply to your own code today.

This guide is for protecting applications you own or maintain. It explains causes and fixes only.

1. SQL injection: never build queries with user input

The mistake: putting request data straight into SQL text.

// Unsafe: user input becomes part of the SQL
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";

The fix: use prepared statements, so the database always treats input as data, never as SQL.

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

With PDO, also set PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION and select only the columns you need.

2. Cross-site scripting (XSS): escape everything you print

The mistake: echoing user content into HTML as it is, so a comment containing a script runs in other people’s browsers.

The fix: escape on output, every time, for the right context.

function e(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo '<p>' . e($comment) . '</p>';

Template engines such as Twig and Blade escape by default; keep that on. Add a Content-Security-Policy header as a second line of defence.

3. CSRF: check a token on every state-changing request

The mistake: a form that changes data (password, email, payment) accepts any POST, so another site can trick a logged-in user’s browser into submitting it.

The fix: a random token per session, placed in the form and verified on submit.

// when rendering the form
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
echo '<input type="hidden" name="csrf" value="' . e($_SESSION['csrf']) . '">';

// when handling the POST
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(403);
    exit('Invalid request');
}

Setting session cookies with SameSite=Lax (or Strict) adds protection too.

4. Weak password storage: use password_hash

The mistake: storing passwords with md5() or sha1(), or worse, in plain text. Those hashes are far too fast to resist cracking.

The fix: PHP’s built-in functions, which choose a strong algorithm and salt for you.

$hash = password_hash($password, PASSWORD_DEFAULT);   // on sign-up
if (password_verify($password, $hash)) { /* login ok */ }
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) { /* re-hash and save */ }

5. Unsafe file uploads: validate, rename, store outside the web root

The mistake: trusting the file name and extension, and saving uploads inside a folder the web server can execute.

The fix:

  • Check the size, and detect the real type from the file content with finfo (not the extension the browser sent).
  • Allow only the types you need (for example JPEG, PNG, PDF).
  • Generate a new random file name; never reuse the uploaded name.
  • Store uploads outside the public web root, or in object storage, and serve them through a script that sets the correct Content-Type.
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->file($_FILES['photo']['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
if (!isset($allowed[$type]) || $_FILES['photo']['size'] > 5_000_000) {
    exit('File not allowed');
}
$name = bin2hex(random_bytes(16)) . '.' . $allowed[$type];
move_uploaded_file($_FILES['photo']['tmp_name'], '/var/app-uploads/' . $name);

6. Session fixation and weak cookies

The fix: create a new session id after login, and set safe cookie flags.

session_set_cookie_params([
    'secure' => true, 'httponly' => true, 'samesite' => 'Lax',
]);
session_start();
// after a successful login:
session_regenerate_id(true);

7. unserialize() on untrusted data

The mistake: calling unserialize() on data that came from a user (a cookie, a form, an API). This can create unexpected objects and lead to serious problems (PHP object injection).

The fix: use JSON for anything from outside: json_decode($input, true). If you must unserialize, pass ['allowed_classes' => false].

The basics that prevent many incidents

  • Stay on a supported PHP version. Old versions stop getting security fixes; check the official supported-versions page.
  • Update dependencies with Composer and run composer audit regularly.
  • Hide errors in production: display_errors = Off, log them instead.
  • Least privilege: the database user for the app should not be able to drop tables.

Quick checklist

RiskFix
SQL injectionPrepared statements (PDO)
XSShtmlspecialchars on output + CSP
CSRFRandom token + hash_equals
Passwordspassword_hash / password_verify
Uploadsfinfo type check, random name, outside web root
Sessionssession_regenerate_id(true), secure cookie flags
Object injectionJSON instead of unserialize

Frequently asked questions

Is md5 safe for storing passwords in PHP?

No. md5 and sha1 are far too fast and easy to crack. Use password_hash with PASSWORD_DEFAULT and password_verify.

Does escaping input prevent SQL injection?

Prepared statements are the reliable fix, because the database treats input strictly as data. Manual escaping is easy to get wrong.

How do I check an uploaded file's real type in PHP?

Use finfo with FILEINFO_MIME_TYPE on the temporary file, allow only the types you need, and rename the file.

Which PHP version should I use?

A version that still receives security fixes. Check the official supported-versions page on php.net.

Sources

About the author. Written by a software engineer and system architect with 10+ years of building production web, payment, ticketing and accounting systems in Japan and Nepal. More

Written with AI assistance, then checked and edited by a human engineer. Found a mistake? Tell us.