Skip to content
Educora
Advanced22 min9 / 10

Forms, sessions and security

Receive form data with $_GET and $_POST, validate it and protect against XSS with htmlspecialchars, work with sessions and cookies, upload files, hash passwords and apply CSRF tokens.

Check yourself
In this lesson you will learn
  • Read form data from superglobals, validate it on the server and redirect after a POST
  • Prevent XSS with htmlspecialchars and 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.

ArrayWhat it holds
$_GETparameters in the URL: search.php?q=php → $_GET['q']
$_POSTfields of a form sent with method="post"
$_SERVERinformation about the request, e.g. $_SERVER['REQUEST_METHOD']
$_COOKIE, $_SESSIONcookies sent by the browser and session data
$_FILESuploaded 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.

Vulnerable: XSS is possible
<p>Hello, <?= $_GET['name'] ?>!</p>
Safe
<p>Hello, <?= htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8') ?>!</p>
With the address ?name=<script>...</script>, the page on the left runs the script, while the one on the right shows it as plain text.
PHP
<?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";
Expected output
&lt;script&gt;alert(&quot;hacked&quot;)&lt;/script&gt;
Tom &amp; Jerry&#039;s &quot;show&quot;

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
<?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)
contact.php: handling the form (needs a web server)

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
<?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],
]));
Expected output
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
<?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";
First visit: Views this session: 1, theme: light. After a refresh: 2, theme: dark — the cookie arrives only with the next request.
  • httponly hides 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 = []; and session_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
<?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!';
}
upload.php: the form sends an <input type="file" name="photo"> field

Passwords 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
<?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));
Expected output
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
<?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
}
In the form: <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">

Key points

  • $_GET holds URL parameters and $_POST form fields; operations that change data are sent with POST.
  • Escape every piece of user text with htmlspecialchars before putting it into HTML — this prevents XSS.
  • Validation happens on the server (filter_var, types); after a successful POST, redirect with header('Location: ...') and exit.
  • 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.

1 / 10
Which function is used to print a user's name into a page without XSS risk?