25. PHP Security Basics

What you will learn

  • The golden rule: never trust input
  • How to stop XSS, SQL injection, and CSRF
  • How to store passwords and protect sessions
  • Safe settings for live websites

Security is a large subject. This lesson covers the most common problems for beginners. It is a starting point, not a guarantee. For anything handling real people’s data, use well-tested frameworks and libraries, keep everything updated, and consider a professional review.

The golden rule

All data from outside your code is untrusted. That includes $_GET, $_POST, $_COOKIE, $_FILES, most of $_SERVER, and data from other websites or APIs. Handle it in three steps:

  1. Validate it on arrival: right type, length, and allowed values.
  2. Use it safely where it goes: prepared statements for the database, safe paths for files.
  3. Escape it when you print it: the method must match the destination.

1. Cross-site scripting (XSS)

If you print visitor text without escaping, an attacker can inject HTML or JavaScript that runs in other visitors’ browsers.

PHP code

<?php
$comment = "<script>stealCookies()</script>Nice post!";

// Dangerous
// echo $comment;

// Safe
echo htmlspecialchars($comment, ENT_QUOTES, "UTF-8");

What the browser shows (the tags appear as harmless text)

<script>stealCookies()</script>Nice post!

  • Escape every value you print, including ones from your own database.
  • Escape for the right place. htmlspecialchars() is for HTML. Values placed in a web address need urlencode(), and data given to JavaScript needs json_encode().
  • In WordPress, use esc_html(), esc_attr(), esc_url(), and friends.

2. SQL injection

Never join input into SQL text. Use prepared statements with placeholders, as in the database lesson.

PHP code

<?php
// Safe: the value travels separately from the command
$stmt = $pdo->prepare("SELECT id, name FROM students WHERE email = :email");
$stmt->execute(["email" => $email]);
$student = $stmt->fetch();

Placeholders protect values. A table or column name that must vary has to come from an allowed list that you write.

3. Cross-site request forgery (CSRF)

A malicious page can make a logged-in visitor’s browser send a form to your site without them knowing. The defence is a random token that only your own forms contain.

PHP code: csrf.php

<?php
function csrf_token(): string
{
    if (empty($_SESSION["csrf"])) {
        $_SESSION["csrf"] = bin2hex(random_bytes(32));
    }
    return $_SESSION["csrf"];
}

function csrf_valid(?string $token): bool
{
    return is_string($token)
        && isset($_SESSION["csrf"])
        && hash_equals($_SESSION["csrf"], $token);
}

PHP code: form.php

<?php
session_start();
require_once __DIR__ . "/csrf.php";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    if (!csrf_valid($_POST["csrf"] ?? null)) {
        http_response_code(400);
        exit("Invalid request.");
    }
    // ...the token was valid, so process the form...
    echo "Form accepted.";
}
?>
<form method="post">
  <input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token(), ENT_QUOTES, "UTF-8") ?>">
  <label for="note">Note</label>
  <input type="text" id="note" name="note">
  <button type="submit">Send</button>
</form>

What happens

Submitting the form normally shows: Form accepted.

Sending a POST without the right token (for example from another site) shows: Invalid request.

random_bytes() makes an unguessable token, and hash_equals() compares it safely. Use POST for anything that changes data, and never change data in response to a GET request. WordPress calls its version of this a nonce.

4. Passwords

Never store a password as plain text, or with fast hashes such as md5() or sha1(). Use the built-in functions, which add a random salt and are deliberately slow.

PHP code

<?php
$hash = password_hash("correct horse battery", PASSWORD_DEFAULT);

var_dump(password_verify("correct horse battery", $hash));
var_dump(password_verify("guess", $hash));
var_dump(password_needs_rehash($hash, PASSWORD_DEFAULT));

What you should see (inside <pre>)

bool(true)
bool(false)
bool(false)
  • Store the hash in a column of at least 255 characters.
  • If password_needs_rehash() returns true after a successful login, hash the password again and update the stored value.
  • Let people use long passphrases, and never email a password back to them.
  • Give login errors a vague wording, such as “Wrong username or password”.

5. Sessions and logins

  • Call session_regenerate_id(true) after login.
  • Check the session at the top of every private page, then exit after redirecting.
  • Mark session cookies secure, httponly, and samesite.
  • Log out by destroying the session completely.
  • Check permissions as well as login. Being logged in does not mean being allowed to edit someone else’s data.

6. Files and uploads

  • Never build a file path from input. Use an allowed list, or basename() plus a fixed folder.
  • Check the real type of uploads, generate your own file names, and save outside the public folder where possible.
  • Never include or run a file chosen by a visitor.

7. Safe settings for a live site

Setting Live site
display_errors Off. Error text can reveal paths and secrets
log_errors On, writing to a file you can read
PHP version A currently supported release. Old versions get no security fixes
HTTPS Always, for the whole site
Passwords and keys Outside the public folder, never in code you share
Test and info pages Delete them. Never leave a page that prints server details

8. HTTPS and security headers

HTTPS encrypts data between the visitor and your server. Hosts such as Hostinger can install a free certificate. Security headers, such as Content-Security-Policy and X-Content-Type-Options, add extra protection. They are usually set in the server settings or by a security plugin, and need testing, because a wrong policy can break your site.

9. Keep everything updated

Most real attacks use known bugs in old software. Keep PHP, WordPress, themes, and plugins updated, remove what you do not use, and take backups before updating.

A quick checklist

  1. Every input is validated.
  2. Every printed value is escaped for its destination.
  3. Every database query with outside data uses placeholders.
  4. Every state-changing form has a CSRF token and uses POST.
  5. Passwords use password_hash().
  6. Errors are hidden from visitors and logged for you.
  7. Secrets are outside the public folder and out of version control.
  8. The site runs on HTTPS with supported software.

Common mistakes

  • Escaping on input and storing damaged text. Escape on output.
  • Thinking “it is only my own admin form, so it is safe”.
  • Trusting hidden fields, drop-down values, or cookies.
  • Using md5() for passwords.
  • Leaving display_errors on in production.
  • Writing your own login or encryption when a tested library exists.

Exercise

This code has three security problems. Find them, and write a safer version.

<?php
$id = $_GET["id"];
$result = $pdo->query("SELECT * FROM users WHERE id = $id");
$user = $result->fetch();
echo "Hello " . $user["name"];
file_put_contents("users.txt", md5($_GET["pass"]));
Show solution

The problems: (1) SQL injection from joining $id into the query, (2) XSS from printing the name without escaping, (3) a weak md5() password hash, sent in a GET request and saved to a file. The password should never travel in the address or sit in a public file.

<?php
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);

if ($id === null || $id === false) {
    exit("Invalid request.");
}

$stmt = $pdo->prepare("SELECT name FROM users WHERE id = :id");
$stmt->execute(["id" => $id]);
$user = $stmt->fetch();

if ($user === false) {
    exit("User not found.");
}

echo "Hello " . htmlspecialchars($user["name"], ENT_QUOTES, "UTF-8");

// Passwords: collect them with a POST form, and store
// password_hash($password, PASSWORD_DEFAULT) in the database.

Knowledge check

1. Which function escapes text for safe printing in HTML?

htmlspecialchars()

2. What protects a database query from SQL injection?

Prepared statements with placeholders.

3. Which functions should you use to store and check passwords?

password_hash() and password_verify().

Summary

  • Treat all outside data as untrusted: validate it, use it safely, and escape it on output.
  • Defend against XSS, SQL injection, and CSRF, and hash passwords properly.
  • Hide errors on live sites, use HTTPS, and keep software updated.
Scroll to Top