XSS и CSRF: защита форм, сессий и куки
Две уязвимости, которые регулярно путают новички и регулярно находят в рабочих проектах. XSS — когда чужой код выполняется в браузере вашего пользователя. CSRF — когда браузер вашего пользователя выполняет чужое действие от его имени. Обе про «доверие не там, где надо», но механизмы у них противоположные: XSS ломает вывод данных, CSRF эксплуатирует слишком много доверия к аутентифицированному пользователю.
Статья практическая: контексты экранирования, CSP, флаги куки, защита сессий в PHP и реальность wp_nonce в WordPress — без разговора «просто экранируйте всё».
Важно. Примеры payload’ов — для понимания защиты. Не используйте их против чужих систем.
Часть I. XSS: межсайтовый скриптинг
1. Чем XSS отличается от SQLi
В SQLi пользовательский ввод превращается в код запроса на стороне сервера. В XSS пользовательский ввод превращается в код (HTML/JS) на стороне браузера жертвы. Разница принципиальная:
- Уязвим не «ваш сервер», а страница, которую открывает другой человек (или сам админ).
- Последствия — не чтение БД напрямую, а действия от имени жертвы: угон сессии через куку, подмена формы, фишинг, запись под админом.
- Поэтому
htmlspecialcharsв одном месте не «закрывает XSS» — код может выводиться в десятках мест и контекстов.
2. Виды XSS
2.1 Reflected (отражённый)
Ввод из запроса сразу попадает в ответ без сохранения:
<?php
// search.php
$term = $_GET['q'];
echo "<p>Результаты по запросу: " . $term . "</p>";?q=<script>alert(1)</script> — скрипт выполнится у того, кто открыл/кликнул ссылку. Классический вектор доставки — ссылка в письме или чате.
2.2 Stored (сохранённый, persistent)
Ввод кладётся в БД, а потом выводится на чужих страницах — без участия атакующего в момент срабатывания:
<?php
// comment.php — коммент сохранён как есть, а выводится так:
echo "<div class='comment'>" . $comment->text . "</div>";Опаснее reflected: один сохранённый payload бьёт по всем читателям страницы и по админке. Худший случай — XSS в профиле пользователя или в поле плагина, которое видит админ в wp-admin (тогда это путь к RCE через редактор плагинов).
2.3 DOM-based
Сервер вообще не причастен. Данные берутся из location/hash и вставляются в DOM:
<script>
// page не проверяет содержимое
const name = decodeURIComponent(location.hash.slice(3)); // "?q=..."
document.getElementById('out').innerHTML = 'Привет, ' + name; // sink
</script>page.html#q=<img src=x onerror=alert(1)> выполнит код. Такие дыры не находят серверным сканером и статанализом PHP — нужен аудит фронтенда (см. раздел 6).
2.4 Self-XSS и mXSS
- Self-XSS — payload вводится самой жертвой (например, в консоль или в спецполе). Работает только в социнженерии («вставьте этот код, чтобы активировать взлом»), но часто обходит строгие фильтры.
- mXSS (mutation XSS) — браузер «чинит» HTML при парсинге, и корректно, казалось бы, экранированный вывод после mutation (
innerHTML→ сериализация → сноваinnerHTML) обрастает исполняемым кодом. Лечится не экранированием, а запретомinnerHTMLна недоверенных данных и CSP.
3. Почему «просто htmlspecialchars» не спасает
htmlspecialchars экранирует & < > " ' для тела HTML. Но браузер исполняет контекстно, а контексты разные:
| Место вывода | Правильная функция | Почему htmlspecialchars мало |
|---|---|---|
| Текст между тегами | htmlspecialchars(ENT_QUOTES) | — |
Значение атрибута в " | htmlspecialchars(ENT_QUOTES) + кавычки в разметке | без кавычек ломается на пробелах |
href="..." / URL-параметр | rawurlencode() | data:text/html,, javascript: не проходят htmlspecialchars |
Строка внутри <script> | json_encode(..., JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT) | ' и " там — не HTML-сущности, а синтаксис JS |
Контент JSON для fetch | header('Content-Type: application/json') + json_encode | текстовый text/html на JSON — источник DOM XSS |
CSS-свойство / inline-style | белый список значений | expression(), url(javascript:) |
Ключевой вывод: функция подбирается по месту вставки, а не по типу данных. Один и тот же $name в теле — через htmlspecialchars, в href — через rawurlencode, в JS — через json_encode.
4. Экранирование по контексту: рабочие сниппеты
4.1 HTML-тело и атрибуты
<?php
declare(strict_types=1);
// Универсальный экранировщик для HTML-контекста
function e(string $v): string
{
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
// ENT_SUBSTITUTE — заменяет битые байты на U+FFFD вместо возврата пустой строки
// (без него invalid UTF-8 -> "" -> молча теряем данные, что само по себе баг).
?>
<p>Результаты по запросу: <?= e($term) ?></p>
<!-- Для атрибута ОБЯЗАТЕЛЬНО кавычки: -->
<img src="/avatar.png" alt="<?= e($altText) ?>">Синтаксис <?= ... ?> выводит уже экранированное значение — короткая и безопасная форма. Никогда не пишите <?php echo $var ?> без e() для чужих данных.
4.2 URL и href
<?php
// 1) Схема — только разрешённые протоколы.
function safe_url(string $url): string
{
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
// Разрешаем относительные URL и http/https.
if ($scheme !== '' && !in_array($scheme, ['http', 'https'], true)) {
return ''; // отсекает javascript:, data:, vbscript: и прочее
}
return $url;
}
// 2) Значение, которое вшивается в query-параметр — кодируется, а не экранируется:
$href = '/search.php?q=' . rawurlencode($term);
?>
<a href="<?= e($href) ?>">Поиск</a>
<!-- Здесь e() уже не нужен для ?q= (rawurlencode сделал), но e() безвреден;
он нужен, чтобы сама ссылка не ломала HTML-атрибут. -->Не забывайте про rel="noopener noreferrer" для target="_blank" к внешним адресам — это защита от reverse-tabnabbing, а не от XSS, но часто всплывает в тех же ссылках.
4.3 Данные для JS и JSON
<?php
// Встроенные в скрипт данные — через json_encode с флагами HEX:
$config = [
'user' => $name,
'csrf' => $token,
'debug' => false,
];
$payload = json_encode(
$config,
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
);
// JSON_HEX_TAG превращает < > в \u003C \u003E, чтобы данные не закрыли </script>
?>
<script id="app-config" type="application/json"><?= $payload ?></script>
<script>
const cfg = JSON.parse(document.getElementById('app-config').textContent);
</script>Отдельный type="application/json" блок (без выполнения) + JSON.parse — безопаснее, чем вшивать значение прямо в JS-строку.
4.4 Фронтенд: DOM без инъекции
// НЕЛЬЗЯ: innerHTML с недоверенными данными
out.innerHTML = 'Привет, ' + name;
// ПРАВИЛЬНО:
out.textContent = 'Привет, ' + name; // вставка как текст
// Если нужен HTML из доверенного шаблона — Template + безопасная подстановка:
const t = document.createElement('template');
t.innerHTML = '<span class="greet"></span>';
t.content.querySelector('.greet').textContent = name;
out.replaceChildren(t.content.cloneNode(true));Для ссылок из данных — a.href = url только после проверки схемы (см. 4.2), а не a.innerHTML.
5. CSP — второй эшелон
Content-Security-Policy — политика, которую браузер применяет к загрузке исполняемых ресурсов. Это не замена экранированию, а страховка: если где-то пропустили XSS, CSP не даст выполниться чужому скрипту.
5.1 Базовый заголовок
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
<?php
$nonce = base64_encode(random_bytes(16));
// nonce должен быть уникален на каждый HTML-ответ
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self'; form-action 'self'");
?>
<script nonce="<?= $nonce ?>">/* ваш легитимный inline-скрипт */</script>nonce— одноразовое случайное значение на ответ; разрешает только помеченные им инлайн-скрипты. Храните nonce только на сервере этого ответа, никогда не в куке/GET.'strict-dynamic'+ nonce/hash отключает whitelist доменов: если скрипт загружает другой скрипт через DOM-программно, цепочка доверия сохраняется. Это и позволяет не ставитьunsafe-inline.object-src 'none'; base-uri 'self'— обязательны, иначеbase-тег переопределит относительные URL и сломает всю защиту.- ⛔ Не добавляйте
unsafe-inlineиunsafe-eval— они обесценивают CSP для XSS. Если легитимный inline-код нужен, используйте nonce/hash.
5.2 Hash для конкретных инлайн-блоков
script-src 'self' 'sha256-1OQnLX...base64hash...=';
Хэш считается от точного текста инлайн-скрипта; любой символ внутри меняет хэш → браузер не выполнит. Годится для пары фиксированных скриптов, неудобно для динамического контента (там nonce).
5.3 Report-Only
Обкатывайте политику на Content-Security-Policy-Report-Only + endpoint, чтобы собрать реальные нарушения до включения строгого режима. Иначе включите CSP и «поломаете» сайт в первый же день.
Важные границы CSP:
- Не защищает от CSRF (
form-actionчастично ограничивает, куда уходит форма, но не заменяет токен). - Не защищает от кликджекинга напрямую (
frame-ancestorsпомогает,X-Frame-Options— устаревшая замена). - Не спасает от XSS в самом «вашем» eval-подобном коде.
6. DOM-XSS: отдельная охота
Серверный статанализ PHP DOM-инъекции не видит. Ищите по схеме source → sink:
- Источники:
location.hash,location.search,document.referrer,name,postMessage-данные без проверкиevent.origin. - Стоки (sinks):
innerHTML,outerHTML,document.write,eval,setTimeout('строка'),element.href/srcс пользовательским URL, jQuery.html(), шаблонизаторы с{{{ }}}(triple-stash без экранирования).
Инструменты: SonarQube/CodeQL (правила DOM-XSS), Semgrep с JS-правилами, ручной grep по фронтенду:
# грубая эвристика по JS/TS: опасные sinks рядом с location/postMessage
grep -rnE 'innerHTML|document\.write|eval\(' static/ | grep -vE 'textContent'
grep -rn 'postMessage' static/ # для каждого — проверить event.origin
7. Куки и сессии PHP: флаги, которые реально важны
7.1 setcookie: три обязательных флага
<?php
setcookie('session_id', $sid, [
'expires' => time() + 3600,
'path' => '/',
'domain' => '', // '' = текущий хост, НЕ '.example.com' (не расшариваем на поддомены)
'secure' => true, // только по HTTPS
'httponly' => true, // недоступна из document.cookie (JS) — тормозит угон через XSS
'samesite' => 'Lax', // не слать на кросс-сайтовые POST; см. раздел 10
]);HttpOnlyне даёт JS прочитать куку — то есть украсть сессию через XSS становится сложнее (но XSS всё равно может действовать от имени жертвы через её же формы — поэтому HttpOnly не панацея, а один слой).Secureзапрещает отправку по http.SameSite— см. Часть II.
7.2 Жёсткая конфигурация сессий через ini
; php.ini — базовый безопасный профиль сессий PHP 8.x
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.use_strict_mode = 1 ; отвергает неизвестные sid от клиента (fixation)
session.use_only_cookies = 1 ; sid только из cookie, не из URL
session.sid_length = 48
session.sid_bits_per_character = 6 ; больше энтропии в ID
7.3 Session fixation и регенерация
Атака: подсовывает жертве заранее известный sid (ссылка с PHPSESSID=... — если use_only_cookies=0), жертва входит под ним, и злоумышленник уже «в сессии». Лечится строго на входе:
<?php
session_start([
'cookie_httponly' => true,
'cookie_secure' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
]);
// ...успешная проверка пароля...
if ($passwordValid) {
// Новый ID + удаление данных старой сессии:
session_regenerate_id(true);
$_SESSION['uid'] = $user->id;
$_SESSION['login_ip'] = $_SERVER['REMOTE_ADDR'] ?? '';
// Привязка к UA — слабый эвристик, часто даёт ложные срабатывания;
// лучше держать fingerprint и сверять при эскалации прав.
}session_regenerate_id(true) — обязательно после входа, смены пароля и восстановления доступа. Без него вход «под чужим сидом» остаётся валидным.
Часть II. CSRF: подделка межсайтовых запросов
8. Механика и когда это вообще срабатывает
Сценарий: пользователь залогинен в bank.example. На evil.example загружается:
<form id="f" action="https://bank.example/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="1000">
</form>
<script>document.getElementById('f').submit();</script>Браузер отправит POST на банк вместе с куками сессии пользователя, потому что запрос «выглядит» как обычный навигационный/сабмит. Банк видит валидную сессию и выполняет перевод. Сервер не может отличить этот запрос от реального сабмита формы.
Условия, при которых CSRF срабатывает (все одновременно):
- Аутентификация держится на куках, которые браузер готов послать автоматически.
- Сервер не проверяет токен/секрет, известный только легитимной форме.
- Действие выполняется на GET/POST, данные которого не нужны «закрытые» браузером.
- Пользователь залогинен в момент визита на чужой сайт.
Где НЕ срабатывает / ослаблено:
- Авторизация через
Authorization: Bearerиз JS — браузер сам его не приложит к чужому сабмиту; но cookie-аутентификация уязвима. SameSite=Lax/Strictна сессионной куке (см. 9) — снимает большую часть POST-случаев, но не все (см. 10).
9. Первая линия: SameSite и проверка Origin
9.1 SameSite — не полная защита
Современные браузеры по умолчанию применяют SameSite=Lax к кукам без явного атрибута. Что это даёт и чего НЕ даёт:
| Атрибут | Отправляет куку | CSRF-эффект |
|---|---|---|
Strict | Только на same-site навигацию | Гасит почти весь CSRF, но ломает UX (клик из письма по GET-ссылке — без сессии) |
Lax | На кросс-сайтовый GET-навигационный запрос | POST-сабмиты с чужого сайта без куки (хорошо), но GET-действия всё ещё уязвимы |
None | Везде (требует Secure) | Никакой CSRF-защиты (нужен токен) |
Отсюда два правила:
- Никогда не выполняйте state-изменяющие действия на GET.
?action=delete&id=5подSameSite=Laxуязвим, потому что Lax пропускает GET-навигацию. SameSite— удобная граница, не замена токену. Разработчики, знающие про дефолтный Lax, иногда убирают токен зря.
9.2 Проверка Origin / Referer
<?php
function csrf_origin_ok(array $allowedHosts = ['example.com', 'www.example.com']): bool
{
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin !== '') {
$host = strtolower((string) parse_url($origin, PHP_URL_HOST));
return $host !== '' && in_array($host, $allowedHosts, true);
}
// Fallback на Referer для простых GET-сабмитов:
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if ($referer === '') {
return false; // без информации — отказ (fail closed)
}
$host = strtolower((string) parse_url($referer, PHP_URL_HOST));
return $host !== '' && in_array($host, $allowedHosts, true);
}Ограничения: Origin шлётся не всегда и может отсутствовать при кросс-домене в старых сценариях; Referer срежется политиками Referrer-Policy: no-referrer. Поэтому проверка origin — второй эшелон перед токеном, а не вместо него.
10. Вторая линия и обязательная база: CSRF-токен (Synchronizer Pattern)
Принцип: форма несёт секрет, который атакующий не может прочитать с чужого сайта (same-origin-политика не даёт ему читать ваш DOM). Сервер сверяет секрет и требует его на всех state-изменяющих запросах.
<?php
session_start();
// Генерация: один токен на сессию (проще) или на форму (строже).
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf'];
?>
<form action="/profile" method="POST">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token, ENT_QUOTES, 'UTF-8') ?>">
<input name="nickname" value="">
<button>Сохранить</button>
</form><?php
// profile.php — приём:
session_start();
$sent = $_POST['csrf_token'] ?? '';
if (
!is_string($sent)
|| empty($_SESSION['csrf'])
|| !hash_equals($_SESSION['csrf'], $sent) // hash_equals — тайминг-безопасное сравнение
) {
http_response_code(403);
exit('Недопустимый токен (CSRF?)');
}
// Проверка токена НЕ заменяет проверку прав!
if (!current_user_can_edit($userId)) {
http_response_code(403);
exit('Нет прав');
}Разбор деталей:
hash_equals, а не==: предотвращает утечку через time-attack при посимвольном сравнении токена.random_bytes(32): крипто-стойкий генератор.mt_rand/uniqid/base-on-time — не токены.- Регенерация: удалите
$_SESSION['csrf']при разлогине и эскалации прав (параллельно сsession_regenerate_id). - Пустое поле/отсутствие — отказ, не «пропустить».
- Токен обязательно сопровождается проверкой авторизации/прав: токен говорит «это наш пользователь и наша форма», права — «можно ли это действие». Один токен без
current_user_can()пропускает горизонтальную эскалацию.
11. Stateless-варианты: double-submit cookie и HMAC
Когда нет серверной сессии (API/микросервисы):
Double-submit cookie. Ставите не-HttpOnly куку с токеном и требуете тот же токен в теле/заголовке; сверяете куку с телом. Атакующий с чужого сайта не прочитает и не подделает вашу куку для конкретного домена… но это требует аккуратности: без SameSite кука уходит на запрос, а значит её нужно уметь отличать. Схему используют с SameSite=Lax/Strict на самой csrf-куке.
HMAC-токен (stateless). Токен = base64(payload) . HMAC(secret, payload), payload = uid|action|expiry. Сервер только проверяет подпись и срок — не хранит состояние:
<?php
function csrf_issue(string $secret, int $uid, string $action, int $ttl = 3600): string {
$exp = time() + $ttl;
$p = "{$uid}|{$action}|{$exp}";
$sig = hash_hmac('sha256', $p, $secret);
return rtrim(strtr(base64_encode($p), '+/', '-_'), '=') . '.' . $sig;
}
function csrf_verify(string $secret, string $token, int $uid, string $action): bool {
[$p64, $sig] = array_pad(explode('.', $token, 2), 2, '');
$p = base64_decode(strtr((string) $p64, '-_', '+/'), true);
if ($p === false) {
return false;
}
$parts = explode('|', $p);
if (count($parts) !== 3) {
return false;
}
[$tokUid, $tokAction, $exp] = $parts;
// Сначала сверяем подпись, затем вложенные uid/action и срок.
return hash_equals(hash_hmac('sha256', $p, $secret), (string) $sig)
&& (int) $tokUid === $uid
&& $tokAction === $action
&& (int) $exp >= time();
}Секрет подписи держите в env (не в БД и не в репозитории). Токен не привязан к сессии — подходит для горизонтально-масштабируемых API без липкого session store.
12. AJAX и CORS: где часто ошибаются
- Для AJAX-запросов с
Authorization-заголовком CSRF почти не работает (браузер сам заголовок не приложит). Если же аутентификация на куках — тот же CSRF-токен нужен в заголовке (X-CSRF-Token), проверяемом сервером так же строго. - ⛔ CORS — это не защита от CSRF. Разрешительный
Access-Control-Allow-Origin: *не помогает, потому что обычный<form>/fetch(no-cors)и так «проходит» (CORS ограничивает чтение ответа, не отправку запроса). Более того,enctype="text/plain"умеет обходить preflight — то есть наивная «политика CORS защитит» ломается. - Кастомный заголовок-«секрет» как CSRF-защита (например,
X-Requested-With): работает, только если атакующий не может его послать;X-Requested-Withсам по себе ненадёжен, используйте токен.
13. Специфика WordPress
13.1 wp_nonce — что это реально такое
Нонсы WordPress — это не классический CSRF-токен с привязкой к сессии, а одноразовое значение HMAC-ish(user_id + session_token + action + time_block), живущее по умолчанию 24 часа (два 12-часовых «тика»):
<?php
// Форма:
wp_nonce_field('my_save', 'my_nonce'); // скрытое поле + action-привязка
// Приём в обработчике:
if (
!isset($_POST['my_nonce'])
|| !wp_verify_nonce($_POST['my_nonce'], 'my_save')
) {
wp_die('Недопустимое значение (403)', 'Forbidden', ['response' => 403]);
}Критично понимать:
wp_verify_nonce()возвращает1(сгенерирован 0–12 ч назад),2(12–24 ч назад) илиfalse. Любое «не false» годится для проверки действия; но «вернул 1» ≠ «да, а 2 ≠ «почти». Пишитеif (! wp_verify_nonce(...) ).- Нонс привязан к пользователю и действию, но не покрывает права: обязательна вторая проверка
current_user_can('...'). Без неё nонс подтверждает «кто-то залогиненный ткнул кнопку», а не «этому можно удалять». - Нонс не отменяет требование «не делайте опасных действий на GET».
- В REST API cookie-аутентификация требует заголовок
X-WP-Nonceс нонсом отwp_rest; без него запрос без аутентификации (или падает), и это тоже надо проверять.
13.2 Правильная пара: нонс + capability
<?php
add_action('admin_post_my_action', 'handle_my_action');
function handle_my_action(): void {
// 1) живём только в POST (опасные действия — не на GET)
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
wp_die('Method not allowed', 'Bad', ['response' => 405]);
}
// 2) нонс
if (!isset($_POST['my_nonce']) || !wp_verify_nonce($_POST['my_nonce'], 'my_save')) {
wp_die('CSRF-проверка не пройдена', 'Forbidden', ['response' => 403]);
}
// 3) права — ОБЯЗАТЕЛЬНО отдельно от нонса
if (!current_user_can('edit_posts')) {
wp_die('Нет прав', 'Forbidden', ['response' => 403]);
}
// 4) только теперь mutate + redirect (Post/Redirect/GET)
do_the_saving();
wp_safe_redirect(admin_url('admin.php?page=my&saved=1'));
exit;
}13.3 Экранирование и вывод в WordPress
Правило из раздела 3–4 сохраняется, просто WP-функциями по контексту:
| Контекст | Функция |
|---|---|
| HTML-тело / атрибут | esc_html(), esc_attr() |
| URL | esc_url() (фильтрует схемы) |
| JS-данные | wp_json_encode() (уже ставит нужные HEX-флаги) |
| Ввод | sanitize_text_field(), wp_kses() для HTML-разметки по allowlist |
innerHTML в админ-скриптах | избегать; или через wp.sanitize / DOMPurify |
esc_html()/esc_attr() под капотом — то же htmlspecialchars с ENT_QUOTES; но не заменяют экранирование в не-HTML контекстах (URL — esc_url, JS — wp_json_encode).
14. Чек-лист
Вывод (XSS):
- Экранирование подбирается по контексту: HTML —
htmlspecialchars(ENT_QUOTES, 'UTF-8'), URL —rawurlencode/esc_url, JS/JSON —json_encodeсJSON_HEX_*. - Ссылки из пользовательских данных проходят проверку схемы (нет
javascript:/data:). - Во фронтенде —
textContent/<template>, никакихinnerHTML/evalна недоверенных данных;postMessageсверяетevent.origin. - Есть CSP с
nonceили'strict-dynamic', безunsafe-inline/unsafe-eval; обкатка через Report-Only;object-src 'none',base-uri 'self',frame-ancestors,form-action 'self'. - Опасные действия не выполняются на GET.
Куки и сессии:
- Сессионная кука:
HttpOnly+Secure+SameSite=Lax(илиStrict, если UX позволяет). session.use_strict_mode=1,session.use_only_cookies=1, вменяемая длина/битность ID.session_regenerate_id(true)на входе, смене пароля, восстановлении доступа; токен/сессия чистятся на logout.domainкуки — точный хост, не.example.com.
Формы и CSRF:
- На всех state-изменяющих запросах — CSRF-токен (сессионный или HMAC-stateless), сравнение через
hash_equals. - Токен не заменяет проверку прав (
current_user_can/ свой authz). - AJAX на cookie-auth несёт токен в заголовке; CORS не считается CSRF-защитой.
SameSiteи проверкаOrigin/Referer— дополнительные слои, не единственные.- WordPress: нонс +
current_user_can+ POST-метод; помним про1/2/falseи RESTX-WP-Nonce.
15. Источники
- OWASP — Cross-Site Scripting Prevention Cheat Sheet, DOM-based XSS Prevention Cheat Sheet
- OWASP — Cross-Site Request Forgery Prevention Cheat Sheet
- OWASP — Session Management Cheat Sheet, Unvalidated Redirects and Forwards
- Content-Security-Reference — CSP: nonce / strict-dynamic
- PHP — setcookie, session, htmlspecialchars, hash_equals
- WordPress — wp_verify_nonce, Nonces (API)
- MDN — SameSite
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
