1. Passwords
Never store a password, or a plain hash of one. Use PHP's built-in functions, which handle salting and cost automatically:
<?php
// registering
$hash = password_hash($password, PASSWORD_DEFAULT);
// store $hash in the database
// logging in
if (password_verify($input, $hash)) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
}md5() and sha1() are not password hashes — they are fast, which
is exactly the wrong property. Modern GPUs test billions per second.
2. SQL injection
Prepared statements, always. See the previous chapter.
3. XSS (cross-site scripting)
Escape everything you print into HTML:
<?php
echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');Without it, a visitor can post <script> that runs for every other
visitor — stealing sessions, defacing pages, submitting forms as them.
Escape at the point of output, and pick the escaping that matches the context (HTML body, attribute, JavaScript, URL).
4. CSRF (cross-site request forgery)
Another site can make a visitor's browser submit a form to yours. Require a token that only your pages know:
<?php
session_start();
// when rendering the form
$_SESSION['csrf'] = $_SESSION['csrf'] ?? bin2hex(random_bytes(32));
?>
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>"><?php
// when handling the POST
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(419);
exit('Invalid request');
}hash_equals() compares in constant time, which avoids leaking the token
through timing.
5. File uploads
<?php
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
$mime = mime_content_type($_FILES['photo']['tmp_name']); // check contents
if (!isset($allowed[$mime])) {
exit('Only JPG and PNG allowed');
}
if ($_FILES['photo']['size'] > 2 * 1024 * 1024) {
exit('Max 2 MB');
}
$name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime]; // ignore user's name
move_uploaded_file($_FILES['photo']['tmp_name'], __DIR__ . '/uploads/' . $name);Store uploads outside the web root where possible, and never trust the extension the browser sent.
6. Randomness
For anything security-related — tokens, password resets, session ids —
use random_bytes() / random_int(). rand() and mt_rand() are
predictable.
<?php
$token = bin2hex(random_bytes(32));7. Keep secrets out of the repository
Database passwords and API keys belong in environment variables or a
.env file that is git-ignored — never committed, never printed in an
error page.