Перейти к содержанию
Educora
Продвинутый22 мин9 / 10

Формы, сессии и безопасность

Принимай данные форм через $_GET и $_POST, проверяй их и защищайся от XSS с помощью htmlspecialchars, работай с сессиями и cookie, загружай файлы, хешируй пароли и применяй CSRF-токены.

Проверь себя
В этом уроке ты узнаешь
  • Читать данные форм из суперглобальных массивов, проверять их на сервере и делать перенаправление после POST
  • Предотвращать XSS с помощью htmlspecialchars и безопасно использовать сессии и cookie
  • Хранить пароли с помощью password_hash, применять CSRF-токен и проверять загружаемые файлы

Форма обратной связи, страница входа, корзина покупок — «живая» часть сайта работает с данными, которые приходят от пользователей. А этим данным никогда нельзя доверять вслепую: кто-то ошибётся случайно, а кто-то сделает их вредоносными нарочно. В этом уроке ты изучишь веб-возможности PHP и основные способы защиты, которые должен знать каждый веб-разработчик.

Суперглобальные массивы

PHP помещает сведения о запросе в особые массивы. Они видны в любом месте кода, даже внутри функций, поэтому называются суперглобальными. Правило: запросы, которые только читают данные (поиск, фильтры), отправляют методом GET, а запросы, которые меняют данные (вход, заказ, комментарий), — методом POST.

МассивЧто содержит
$_GETпараметры в адресе: search.php?q=php → $_GET['q']
$_POSTполя формы, отправленной с method="post"
$_SERVERсведения о запросе, например $_SERVER['REQUEST_METHOD']
$_COOKIE, $_SESSIONcookie, присланные браузером, и данные сессии
$_FILESзагруженные файлы

XSS и htmlspecialchars

Представь, что в гостевой книге кто-то вместо имени написал тег <script>. Если вывести его на страницу как есть, этот скрипт выполнится в браузере каждого посетителя: он может украсть cookie или изменить страницу. Такая атака называется XSS (cross-site scripting). Защита проста: экранируй каждый текст от пользователя функцией htmlspecialchars, прежде чем вставлять его в HTML.

Уязвимо: возможна XSS
<p>Hello, <?= $_GET['name'] ?>!</p>
Безопасно
<p>Hello, <?= htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8') ?>!</p>
При адресе ?name=<script>...</script> страница слева выполнит скрипт, а справа — покажет его как обычный текст.
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";
Ожидаемый результат
&lt;script&gt;alert(&quot;hacked&quot;)&lt;/script&gt;
Tom &amp; Jerry&#039;s &quot;show&quot;

Обработка и проверка формы

Проверки в браузере вроде required нужны только для удобства — их легко обойти. Настоящая проверка выполняется на сервере. После успешного POST перенаправь пользователя на другую страницу с помощью header('Location: ...') и вызови exit. Это шаблон PRG (Post/Redirect/Get): при обновлении страницы форма не отправляется повторно.

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: обработка формы (нужен веб-сервер)

Для проверки удобна функция filter_var: для корректного значения она возвращает само значение (при необходимости приведённое к нужному типу), а для неверного — false. Её можно попробовать и в терминале:

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],
]));
Ожидаемый результат
string(17) "aysel@example.com"
bool(false)
int(42)
bool(false)

Сессии и cookie

HTTP «не помнит»: каждый запрос ничего не знает о предыдущем. Узнать пользователя можно двумя способами. Cookie — небольшие данные, которые хранятся в браузере и отправляются обратно на сервер с каждым запросом. Сессия хранит данные на сервере, а браузер получает только идентификатор сессии (cookie PHPSESSID). session_start() и setcookie() отправляют HTTP-заголовки, поэтому их нужно вызывать до того, как на страницу будет выведен любой текст.

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";
При первом открытии: Views this session: 1, theme: light. После обновления: 2, theme: dark — cookie приходит только со следующим запросом.
  • httponly скрывает cookie от JavaScript — даже при XSS её труднее украсть. На сайте с HTTPS добавь ещё 'secure' => true.
  • При входе пользователя вызови session_regenerate_id(true), затем запиши $_SESSION['user_id'] — старый идентификатор станет бесполезным.
  • При выходе пиши $_SESSION = []; и session_destroy();.
  • Не храни секреты в cookie: пользователь может их увидеть и изменить. Секретные данные остаются в сессии.

Загрузка файлов

У формы, отправляющей файл, должны быть method="post" и enctype="multipart/form-data". PHP кладёт файл во временную папку, а сведения о нём записывает в $_FILES. Проверь размер и настоящий тип файла, дай ему новое случайное имя и только потом перемести его функцией 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: форма отправляет поле <input type="file" name="photo">

Пароли и CSRF

Никогда не храни пароли в открытом виде или с помощью md5 и sha1 — такие хеши взламываются очень быстро. password_hash хеширует пароль специально медленным алгоритмом (сейчас это bcrypt) со случайной «солью» (salt), а password_verify проверяет его. При каждом вызове хеш получается другим, поэтому сравнивать хеши через === нельзя. В базе данных отведи под хеш столбец VARCHAR(255).

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));
Ожидаемый результат
bool(true)
bool(false)
bool(false)

При атаке CSRF (cross-site request forgery) чужой сайт отправляет форму на твой сайт от имени браузера пользователя — например, чтобы сменить его пароль. Браузер автоматически добавляет cookie сессии, поэтому запрос выглядит «законным». Для защиты в сессии создают случайный токен, вставляют его в каждую POST-форму скрытым полем и проверяют во входящем запросе.

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
}
В форме: <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">

Главное

  • $_GET хранит параметры адреса, $_POST — поля формы; операции, меняющие данные, отправляют через POST.
  • Экранируй любой текст от пользователя через htmlspecialchars перед выводом в HTML — это предотвращает XSS.
  • Проверка выполняется на сервере (filter_var, типы); после успешного POST перенаправляй через header('Location: ...') и exit.
  • Данные сессии хранятся на сервере, а cookie — в браузере; session_start() вызывают до любого вывода.
  • Пароли защищают password_hash/password_verify, формы — CSRF-токен, а загрузки — проверка размера и настоящего типа.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какая функция используется, чтобы вывести имя пользователя на страницу без риска XSS?