Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Security

A site is open to the whole internet, and sooner or later someone will try to break into it. Most break-ins exploit the same mistake: the program trusts data sent by the user. The main rule is that any outside data is considered dangerous until it has been checked.

XSS and escaping

If user text is printed on a page as is, it can carry a script that runs for other visitors. This is called XSS. The htmlspecialchars() function turns dangerous characters into harmless ones:

<?php
 
$comment = '<script>alert("Hacked")</script>';
 
echo htmlspecialchars($comment);

The browser shows the script as text but does not run it. Escape everything you print into HTML.

SQL injection

If a database query is glued together from strings, a user can change its meaning. The defence is prepared statements from the database lesson:

<?php
 
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
$pdo->exec("INSERT INTO users (email) VALUES ('anna@example.com')");
 
$email = "' OR '1'='1";
 
$statement = $pdo->prepare('SELECT COUNT(*) FROM users WHERE email = ?');
$statement->execute([$email]);
 
echo $statement->fetchColumn(); // 0

The value travels separately from the query, so the attack attempt is searched for as a plain string and finds nothing.

Password hashing

Passwords must not be stored in plain text: if the database leaks, an attacker gets them all. A hash is stored instead — an irreversible fingerprint of the password:

<?php
 
$hash = password_hash('secret-password', PASSWORD_DEFAULT);
 
var_dump(password_verify('secret-password', $hash)); // bool(true)
var_dump(password_verify('other-password', $hash));  // bool(false)

password_hash() gives a different result every time for the same password — that is how it should be. Only password_verify() checks a password.

The CSRF token

Another site can silently submit a form on behalf of a logged-in user. To tell your own form from a foreign one, add a random token to it and compare it when handling the request:

<?php
 
session_start();
 
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
 
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
        http_response_code(419);
        exit('Invalid token');
    }
}

The token goes into the form as a hidden field: <input type="hidden" name="csrf" value="...">.

Validating input

Check the type and format of everything that comes from the user. The filter_var() function is made for this:

<?php
 
var_dump(filter_var('anna@example.com', FILTER_VALIDATE_EMAIL) !== false); // bool(true)
var_dump(filter_var('42', FILTER_VALIDATE_INT));                           // int(42)
var_dump(filter_var('forty-two', FILTER_VALIDATE_INT));                    // bool(false)

Checklist

ThreatDefence
XSShtmlspecialchars() on output
SQL injectionPrepared statements
Stolen passwordspassword_hash() and password_verify()
CSRFA token in every form
Intercepted dataHTTPS across the whole site

Common mistake

Validation in the browser — the required attribute, JavaScript — is for convenience, not protection. A request can be sent bypassing the form, so every check must be repeated on the server.

Thanks for reading!

Donate to the site — we work hard for you.