PHP security: 7 common vulnerabilities and how to fix them
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.
| SQL injection | PDO prepared statements |
|---|---|
| XSS | htmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE, UTF-8) + CSP |
| CSRF | random_bytes token + hash_equals |
| Passwords | password_hash / password_verify |
| Uploads | finfo type check, random name, outside web root |
| Sessions | session_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 auditregularly. - 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
| Risk | Fix |
|---|---|
| SQL injection | Prepared statements (PDO) |
| XSS | htmlspecialchars on output + CSP |
| CSRF | Random token + hash_equals |
| Passwords | password_hash / password_verify |
| Uploads | finfo type check, random name, outside web root |
| Sessions | session_regenerate_id(true), secure cookie flags |
| Object injection | JSON 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
- OWASP Top 10
- PHP manual: PDO prepared statements
- PHP manual: password_hash
- PHP: supported versions
- IPA: How to secure your website (安全なウェブサイトの作り方)
Written with AI assistance, then checked and edited by a human engineer. Found a mistake? Tell us.