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(); // 0The 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
| Threat | Defence |
|---|---|
| XSS | htmlspecialchars() on output |
| SQL injection | Prepared statements |
| Stolen passwords | password_hash() and password_verify() |
| CSRF | A token in every form |
| Intercepted data | HTTPS 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.