- Read form data from superglobals, validate it on the server and redirect after a POST
- Prevent XSS with
htmlspecialcharsand use sessions and cookies safely - Store passwords with
password_hash, apply a CSRF token and check uploaded files
A contact form, a login page, a shopping cart — the living part of a website works with data that comes from users. And that data must never be trusted blindly: someone may type it wrong by accident, and someone else may make it harmful on purpose. In this lesson you will learn PHP's web features and the basic defences every web developer must know.
Superglobals
PHP puts information about the request into special arrays. They are visible anywhere in the code, even inside functions, which is why they are called superglobals. The rule: requests that only read data (search, filters) are sent with GET, and requests that change data (login, an order, a comment) are sent with POST.
| Array | What it holds |
|---|---|
$_GET | parameters in the URL: search.php?q=php → $_GET['q'] |
$_POST | fields of a form sent with method="post" |
$_SERVER | information about the request, e.g. $_SERVER['REQUEST_METHOD'] |
$_COOKIE, $_SESSION | cookies sent by the browser and session data |
$_FILES | uploaded files |
XSS and htmlspecialchars
Imagine that in a guestbook someone typed a <script> tag instead of their name. If you print it into the page as it is, this script runs in every visitor's browser: it can steal cookies or change the page. This attack is called XSS (cross-site scripting). The defence is simple: escape every piece of user text with htmlspecialchars before putting it into HTML.
<p>Hello, <?= $_GET['name'] ?>!</p><p>Hello, <?= htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8') ?>!</p>?name=<script>...</script>, the page on the left runs the script, while the one on the right shows it as plain text.<?php
$input = '<script>alert("hacked")</script>';
echo htmlspecialchars($input), "\n";
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
echo e("Tom & Jerry's \"show\""), "\n";<script>alert("hacked")</script> Tom & Jerry's "show"
Handling and validating a form
Checks in the browser such as required are only for convenience — they are easy to bypass. The real validation happens on the server. After a successful POST, redirect to another page with header('Location: ...') and call exit. This is the PRG (Post/Redirect/Get) pattern: when the user refreshes the page, the form is not sent a second time.
<?php
session_start();
$errors = [];
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (mb_strlen($name) < 2) {
$errors[] = 'Name is too short';
}
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errors[] = 'Email is not valid';
}
if ($errors === []) {
// save the message to the database here
$_SESSION['flash'] = "Thanks, $name!";
header('Location: /contact.php');
exit;
}
}
// below: the HTML form shows $errors and refills the fields with e($name)The filter_var function is handy for validation: for a valid value it returns the value (converted to the right type if needed), and for an invalid one it returns false. You can try it in the terminal too:
<?php
var_dump(filter_var("aysel@example.com", FILTER_VALIDATE_EMAIL));
var_dump(filter_var("aysel@", FILTER_VALIDATE_EMAIL));
var_dump(filter_var("42", FILTER_VALIDATE_INT));
var_dump(filter_var("200", FILTER_VALIDATE_INT, [
"options" => ["min_range" => 1, "max_range" => 120],
]));string(17) "aysel@example.com" bool(false) int(42) bool(false)
Sessions and cookies
HTTP is “stateless”: each request knows nothing about the previous one. There are two ways to recognise a user. A cookie is a small piece of data stored in the browser and sent back to the server with every request. A session keeps the data on the server, and the browser gets only the session ID (the PHPSESSID cookie). session_start() and setcookie() send HTTP headers, so they must be called before any text is output to the page.
<?php
session_start();
$_SESSION['views'] = ($_SESSION['views'] ?? 0) + 1;
setcookie('theme', 'dark', [
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
$theme = $_COOKIE['theme'] ?? 'light';
echo "Views this session: {$_SESSION['views']}, theme: $theme";Views this session: 1, theme: light. After a refresh: 2, theme: dark — the cookie arrives only with the next request.httponlyhides the cookie from JavaScript — even if XSS happens, it is harder to steal. On an HTTPS site also add'secure' => true.- When a user logs in, call
session_regenerate_id(true), then set$_SESSION['user_id']— the old ID becomes useless. - On logout write
$_SESSION = [];andsession_destroy();. - Never keep secrets in a cookie: the user can see and change it. Secret data stays in the session.
File uploads
A form that sends a file must have method="post" and enctype="multipart/form-data". PHP puts the file into a temporary folder and writes its details into $_FILES. Check the file's size and real type, give it a new random name, and only then move it with move_uploaded_file.
<?php
$file = $_FILES['photo'] ?? null;
if ($file && $file['error'] === UPLOAD_ERR_OK) {
if ($file['size'] > 2 * 1024 * 1024) {
exit('File is larger than 2 MB');
}
$type = mime_content_type($file['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
if (!isset($allowed[$type])) {
exit('Only JPG and PNG images are allowed');
}
$newName = bin2hex(random_bytes(8)) . '.' . $allowed[$type];
move_uploaded_file($file['tmp_name'], __DIR__ . '/uploads/' . $newName);
echo 'Uploaded!';
}<input type="file" name="photo"> fieldPasswords and CSRF
Never store passwords in plain text or with md5 or sha1 — those hashes are cracked very quickly. password_hash hashes a password with a deliberately slow algorithm (currently bcrypt) and a random salt, and password_verify checks it. The hash is different on every call, so you cannot compare hashes with ===. In the database, give the hash a VARCHAR(255) column.
<?php
$hash = password_hash("secret123", PASSWORD_DEFAULT);
var_dump(password_verify("secret123", $hash));
var_dump(password_verify("Secret123", $hash));
var_dump($hash === password_hash("secret123", PASSWORD_DEFAULT));bool(true) bool(false) bool(false)
In a CSRF (cross-site request forgery) attack, another website submits a form to your site on behalf of the user's browser — for example, to change their password. Because the browser adds the session cookie automatically, the request looks “legitimate”. To defend, you create a random token in the session, put it into every POST form as a hidden field and check it in the incoming request.
<?php
session_start();
// 1. create a token once per session
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// 2. every POST form gets a hidden field named csrf with this value
// 3. check the token when a form is submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sent = $_POST['csrf'] ?? '';
if (!hash_equals($_SESSION['csrf'], $sent)) {
http_response_code(403);
exit('Invalid CSRF token');
}
// the form is safe to process
}<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">Key points
$_GETholds URL parameters and$_POSTform fields; operations that change data are sent withPOST.- Escape every piece of user text with
htmlspecialcharsbefore putting it into HTML — this prevents XSS. - Validation happens on the server (
filter_var, types); after a successfulPOST, redirect withheader('Location: ...')andexit. - Session data is stored on the server and cookies in the browser;
session_start()must be called before any output. - Protect passwords with
password_hash/password_verify, forms with a CSRF token, and uploads with size and real-type checks.
Check yourself
10 questions. Every correct answer earns XP.