PHP Security Essentials

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
<?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
<?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
<?php
session_start();

// when rendering the form
$_SESSION['csrf'] = $_SESSION['csrf'] ?? bin2hex(random_bytes(32));
?>
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
PHP
<?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
<?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
<?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.