CSRF и XSS: полное руководство по двум классическим уязвимостям веб-приложений

Введение
Каждый день мы входим в десятки сервисов: почту, банк, соцсети, учебные платформы, интернет-магазины. Всё это работает на доверии. Сайт доверяет браузеру, который передаёт ему cookie с идентификатором сессии. Браузер доверяет сайту и выполняет JavaScript, который сайт ему прислал. Пользователь доверяет и тому и другому.
Атаки XSS и CSRF строятся на злоупотреблении этим доверием. Обе много лет входят в число самых распространённых веб-уязвимостей и регулярно попадают в рейтинг OWASP Top 10 (XSS сейчас относится к категории Injection, а CSRF связан с Broken Access Control). Несмотря на возраст, эти ошибки допускают и новички, и опытные команды.
В этой статье мы разберём:
- как устроены обе атаки и чем они принципиально отличаются;
- какие бывают виды и сценарии;
- как защищаться на уровне кода, заголовков и архитектуры;
- как проверить своё приложение и какие ошибки встречаются чаще всего.
Часть 1. Основы: почему браузер вообще уязвим
Чтобы понять обе атаки, нужно знать три базовых механизма браузера.
1. Cookie отправляются автоматически. Если вы вошли на сайт bank.example, браузер будет прикладывать ваши cookie к каждому запросу на этот домен, независимо от того, с какой страницы запрос инициирован. Этот факт лежит в основе CSRF.
2. Same-Origin Policy (SOP). Скрипт с одного источника (схема + домен + порт) не может свободно читать данные другого источника. Но SOP ограничивает чтение, а не отправку запросов. Именно поэтому чужая страница может отправить запрос на ваш банк (CSRF), но не может прочитать ответ.
3. Браузер исполняет любой JavaScript, который находится на странице. Он не различает «скрипт, который написал разработчик» и «скрипт, который подставил злоумышленник через поле комментария». Если код оказался в HTML страницы, он выполнится с правами этого сайта. Это основа XSS.
Часть 2. XSS (Cross-Site Scripting)
Что это такое
XSS возникает, когда приложение включает в страницу данные, полученные от пользователя, без надлежащей обработки, и браузер интерпретирует их как код. Злоумышленник получает возможность выполнить свой JavaScript в контексте доверенного сайта, то есть с теми же правами, что и у самого сайта.
Простейший учебный пример: поиск по сайту выводит строку запроса на странице: «Результаты по запросу: …». Если запрос вставляется в HTML как есть, то вместо обычного слова в него можно подставить тег <script>, и браузер выполнит его содержимое.
Виды XSS
Stored XSS (хранимый). Вредоносные данные сохраняются на сервере: в комментарии, имени профиля, отзыве, сообщении чата. Каждый, кто открывает страницу, автоматически получает вредоносный код. Это самый опасный вид, потому что жертве не нужно ничего делать, кроме как зайти на страницу.
Reflected XSS (отражённый). Данные приходят в запросе (обычно в параметре URL) и сразу «отражаются» в ответе. Чтобы атака сработала, жертву нужно убедить перейти по специально сформированной ссылке: через письмо, мессенджер или сокращатель ссылок.
DOM-based XSS. Уязвимость целиком находится в клиентском JavaScript. Сервер может быть безупречным, но скрипт страницы берёт данные из location.hash, location.search или document.referrer и небезопасно записывает их в DOM (через innerHTML, document.write, eval и подобное).
Blind XSS. Разновидность хранимого XSS, при которой вредоносный код срабатывает не у самого атакующего, а в закрытой части системы, например в админ-панели, где сотрудник просматривает заявки или обращения пользователей. Из-за высоких привилегий администраторов последствия могут быть особенно серьёзными.
Чем опасен XSS
Выполняя код в контексте сайта, злоумышленник может:
- украсть сессионные cookie (если они не защищены флагом HttpOnly) и войти под видом жертвы;
- читать данные страницы: личные сообщения, номера счетов, содержимое форм;
- выполнять действия от имени пользователя: отправлять сообщения, менять настройки, совершать переводы;
- подменять содержимое страницы, например показать поддельную форму входа прямо на настоящем сайте;
- перехватывать ввод с клавиатуры на странице;
- распространять себя дальше, то есть создавать самораспространяющихся «червей».
Классический исторический пример: в 2005 году червь Samy на MySpace за считанные часы добавил своего автора в друзья более чем миллиону пользователей. Червь использовал хранимый XSS в профилях. Этот случай стал хрестоматийным и наглядно показал, как быстро может распространяться такая уязвимость.
Как защититься от XSS
Главный принцип: любые данные, пришедшие извне, считаются недоверенными, пока не доказано обратное, и перед выводом в страницу должны быть обработаны в соответствии с контекстом.
1. Контекстное экранирование вывода (output encoding). Это главная мера. Один и тот же символ опасен по-разному в разных местах страницы, поэтому способ экранирования зависит от контекста:
Контекст выводаЧто нужно сделатьТело HTMLЗаменять <, >, &, кавычки на HTML-сущностиЗначение атрибутаЭкранировать кавычки, всегда заключать значение в кавычкиJavaScriptНе вставлять данные в код напрямую; передавать их как JSON или через data-атрибутыURLПрименять URL-кодирование и проверять схему (запретить javascript:)CSSНе подставлять недоверенные данные в стили
2. Используйте шаблонизаторы и фреймворки с автоэкранированием. Современные инструменты (React, Vue, Angular, Django Templates, Jinja2, Twig, Razor) экранируют вывод по умолчанию. Опасность возникает, когда разработчик сознательно обходит защиту: dangerouslySetInnerHTML в React, v-html в Vue, фильтр |safe в Django и Jinja2, bypassSecurityTrust... в Angular. Каждое такое место должно быть на особом контроле при код-ревью.
3. Безопасные методы работы с DOM.
javascript
// Небезопасно: данные интерпретируются как HTML
element.innerHTML = userInput;
// Безопасно: данные считаются текстом
element.textContent = userInput;
Избегайте eval(), new Function(), setTimeout("строка"), document.write().
4. Санитизация HTML, когда HTML действительно нужен. Если пользователям разрешено форматирование (редактор статей, комментарии с разметкой), используйте проверенную библиотеку, например DOMPurify. Не пишите собственные «фильтры» на регулярных выражениях: их почти всегда удаётся обойти.
5. Content Security Policy (CSP). Это заголовок, который сообщает браузеру, откуда разрешено загружать и выполнять скрипты. Даже если в страницу попал вредоносный код, строгая политика может помешать ему выполниться.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-СЛУЧАЙНОЕ_ЗНАЧЕНИЕ'; object-src 'none'; base-uri 'none'
Рекомендации по CSP:
- избегайте 'unsafe-inline' и 'unsafe-eval' в script-src;
- используйте nonce или hash для необходимых встроенных скриптов;
- сначала включите режим Content-Security-Policy-Report-Only, чтобы увидеть нарушения и не сломать сайт;
- помните, что CSP это дополнительный слой, а не замена экранированию.
6. Флаги cookie. HttpOnly запрещает JavaScript читать cookie, поэтому даже при XSS украсть идентификатор сессии через document.cookie не получится. Флаг Secure ограничивает передачу cookie только по HTTPS. Учтите, что при XSS злоумышленник всё равно может отправлять запросы от имени пользователя прямо из его браузера, так что HttpOnly снижает ущерб, но не устраняет проблему.
7. Trusted Types. Современный механизм браузеров (Chromium), который заставляет код проходить через специальные политики перед записью в «опасные» места вроде innerHTML. Хорошо подходит для защиты от DOM-based XSS в крупных кодовых базах.
8. Валидация входных данных. Проверяйте тип, длину, формат. Но помните: валидация уменьшает поверхность атаки, а защищает от XSS именно экранирование при выводе.
Часть 3. CSRF (Cross-Site Request Forgery)
Что это такое
CSRF (подделка межсайтового запроса) заставляет браузер аутентифицированного пользователя отправить нежелательный запрос на сайт, где пользователь уже вошёл в систему. Сервер видит корректные cookie и считает, что запрос сделал сам пользователь.
Важная особенность: атакующий не видит ответ на запрос и не крадёт данные напрямую. Он заставляет жертву совершить действие: сменить e-mail, изменить пароль, выполнить перевод, удалить аккаунт, изменить настройки.
Аналогия. Вы подписали банковское поручение и оставили ручку на столе. Кто-то подсунул вам другое поручение. Подпись настоящая, но вы не собирались подписывать именно этот документ. Сервер проверяет только «подпись» (cookie), но не спрашивает, хотели ли вы совершить это действие.
Условия для успешной CSRF-атаки
Обычно нужны три вещи:
- Есть значимое действие, которое можно выполнить запросом (смена e-mail, перевод, изменение настроек).
- Аутентификация построена на cookie (или другом механизме, который браузер подставляет автоматически, например Basic Auth).
- Запрос предсказуем: атакующий знает все параметры и не должен угадывать секретные значения.
Пример сценария
- Пользователь входит в личный кабинет shop.example. Сессионные cookie сохранены в браузере.
- В соседней вкладке он открывает страницу, ссылку на которую получил в чате или письме.
- Страница содержит скрытую форму или запрос, нацеленный на shop.example/account/change-email, который отправляется автоматически.
- Браузер прикладывает cookie, сервер видит «авторизованный» запрос и меняет e-mail на адрес злоумышленника.
- Злоумышленнику остаётся запросить восстановление пароля на новый адрес, и аккаунт захвачен.
Реальные примеры такого рода: массовые атаки на домашние роутеры, когда через CSRF в веб-интерфейсе подменяли настройки DNS, а также уязвимости в социальных сетях и интернет-банках, где одно скрытое действие меняло настройки аккаунта.
Почему GET-запросы особенно опасны
Если приложение выполняет изменяющее действие по GET-запросу (например, /delete?id=5), то для атаки достаточно даже обычной картинки или ссылки на странице. Поэтому изменение состояния должно выполняться только методами POST, PUT, PATCH или DELETE, а GET должен оставаться безопасным и не менять данные.
Как защититься от CSRF
1. CSRF-токены (synchronizer token pattern). Сервер генерирует случайный, непредсказуемый токен, привязанный к сессии пользователя, встраивает его в формы и проверяет при каждом изменяющем запросе. Атакующий не знает этот токен и не может его прочитать из-за Same-Origin Policy, поэтому подделать запрос не сможет.
html
<form method="POST" action="/change-email">
<input type="hidden" name="csrf_token" value="a8f3...случайная_строка...91c2">
<input type="email" name="email">
<button>Сохранить</button>
</form>
Правила для токенов: достаточная длина и криптографическая случайность; привязка к сессии; проверка на сервере для всех изменяющих запросов; токен не должен передаваться в URL.
2. Double Submit Cookie. Альтернатива для приложений без серверного хранилища сессий: токен передаётся и в cookie, и в теле или заголовке запроса, а сервер сравнивает их. Для надёжности токен лучше подписывать или привязывать к сессии.
3. Атрибут SameSite для cookie. Этот атрибут указывает браузеру, когда отправлять cookie при межсайтовых запросах.
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
- Strict: cookie не отправляются при любых межсайтовых запросах (максимальная защита, но может раздражать пользователей при переходах по внешним ссылкам);
- Lax: cookie отправляются только при безопасных «верхнеуровневых» переходах (GET по ссылке), но не при межсайтовых POST и запросах из встроенных элементов. Большинство современных браузеров применяют Lax по умолчанию;
- None: cookie отправляются всегда (требует Secure).
SameSite это отличный дополнительный слой, но не полная замена токенам: поведение зависит от браузера, а некоторые сценарии (например, поддомены, обход через GET-запросы с изменением состояния) остаются рискованными.
4. Проверка заголовков Origin и Referer. Для изменяющих запросов сервер может проверять, что Origin (или Referer) соответствует ожидаемому домену. Современные браузеры отправляют заголовок Origin с POST-запросами, и это хороший дополнительный фильтр. Заголовок Sec-Fetch-Site тоже помогает отличить межсайтовые запросы от обычных.
5. Пользовательские заголовки для API. Для AJAX и API можно требовать собственный заголовок (например, X-Requested-With или токен в Authorization). Межсайтовая отправка таких заголовков через простую форму невозможна, а для JavaScript она потребует CORS-разрешения. Токены в заголовке Authorization (Bearer) вообще не подставляются браузером автоматически, поэтому такие API по природе устойчивы к классическому CSRF (но уязвимы к краже токена при XSS).
6. Повторная аутентификация для критичных операций. Смена пароля, e-mail, платёж, удаление аккаунта: просите ввести пароль, код из SMS или подтвердить через приложение-аутентификатор.
7. Не полагайтесь на CORS как на защиту от CSRF. CORS ограничивает чтение ответа, но «простые» запросы (например, форма POST) уходят на сервер и выполняются независимо от CORS.
8. Используйте встроенную защиту фреймворков. Django (CsrfViewMiddleware), Laravel (@csrf), Spring Security, ASP.NET Core (antiforgery), Rails (protect_from_forgery) включают защиту по умолчанию. Главное не отключать её «чтобы заработало» и не забывать добавлять токены в AJAX-запросы.
Часть 4. XSS против CSRF: сравнение
XSSCSRFСутьВыполнение чужого кода на доверенном сайтеОтправка нежелательного запроса от имени пользователяЧто эксплуатируетсяДоверие пользователя (браузера) к сайтуДоверие сайта к браузеру пользователяГде выполняется кодВ браузере жертвы, на домене уязвимого сайтаКод атакующего выполняется на стороннем сайте; запрос уходит на целевойМожет ли читать данныеДа, в контексте страницыНет, только вызывать действияНужна ли аутентификацияНе обязательноДа, жертва должна быть авторизованаОсновная защитаЭкранирование вывода, CSPCSRF-токены, SameSiteДополнительная защитаHttpOnly, Trusted Types, санитизацияПроверка Origin, повторное подтверждение
Ключевая связь между ними: XSS обесценивает защиту от CSRF. Если злоумышленник может выполнять скрипт на вашем сайте, он способен прочитать CSRF-токен прямо со страницы и отправить корректный запрос. Поэтому устранение XSS это обязательное условие надёжной защиты от CSRF.
Часть 5. Как проверить своё приложение
Для XSS:
- найдите все места, где пользовательские данные попадают на страницу: поля поиска, комментарии, профили, параметры URL, заголовки, имена загружаемых файлов;
- проверьте, что специальные символы (< > " ' &) отображаются как текст, а не интерпретируются как разметка;
- проверьте DOM-сценарии: данные из location.hash и location.search;
- используйте статический анализ кода (SAST) и линтеры, которые ищут innerHTML, eval, dangerouslySetInnerHTML;
- используйте сканеры и прокси вроде OWASP ZAP или Burp Suite (только на собственных системах или с письменного разрешения).
Для CSRF:
- перечислите все запросы, меняющие состояние, и убедитесь, что для каждого есть токен или другая защита;
- проверьте, что запрос без токена или с чужим токеном отклоняется;
- проверьте, нет ли изменяющих действий по GET;
- убедитесь, что у cookie выставлен SameSite;
- проверьте, что токен не переиспользуется между пользователями и не передаётся в URL.
Где безопасно тренироваться: OWASP Juice Shop, DVWA (Damn Vulnerable Web Application), лаборатории PortSwigger Web Security Academy, WebGoat. Это специально уязвимые среды, созданные для обучения. Тестируйте чужие системы только при наличии явного разрешения владельца.
Часть 6. Частые ошибки разработчиков
- «У нас есть фильтр, значит мы защищены». Чёрные списки («удалим слово script») обходятся десятками способов. Защита строится на экранировании и белых списках.
- Экранирование «на входе» вместо «на выходе». Данные нужно экранировать в момент вывода и с учётом контекста, а не при сохранении в базу.
- Отключённая защита фреймворка. CSRF-проверку отключают «временно» при отладке и забывают включить обратно.
- Токен есть, но не проверяется. Токен генерируется и вставляется в форму, а на сервере проверка отсутствует или выполняется не для всех методов.
- Изменение данных через GET.
- Доверие к «внутренним» источникам. Данные из базы, сторонних API, имена файлов, заголовки HTTP тоже могут содержать вредоносный код.
- Слишком мягкий CSP (unsafe-inline, * в источниках), который формально есть, но ничего не защищает.
- Хранение токенов в localStorage без понимания последствий: при любом XSS они будут украдены.
Часть 7. Чек-лист разработчика
XSS
- Весь вывод экранируется с учётом контекста
- Используется шаблонизатор с автоэкранированием
- В коде нет innerHTML, eval, document.write с недоверенными данными
- Пользовательский HTML проходит через DOMPurify или аналог
- Настроен строгий CSP (без unsafe-inline)
- Cookie сессии имеют HttpOnly и Secure
- Проведён аудит всех мест с dangerouslySetInnerHTML, v-html, |safe
CSRF
- Все изменяющие запросы защищены токеном
- Cookie имеют атрибут SameSite (Lax или Strict)
- Изменение состояния только через POST/PUT/PATCH/DELETE
- Проверяются Origin / Sec-Fetch-Site
- Критичные операции требуют повторного подтверждения
- Встроенная защита фреймворка не отключена
Часто задаваемые вопросы
Если я использую React, нужно ли беспокоиться об XSS?
Да, хотя риск ниже. React экранирует вывод по умолчанию, но XSS возможен через dangerouslySetInnerHTML, небезопасные URL в href (например, javascript:), сторонние библиотеки и серверный рендеринг с ошибками.
Если у меня API с JWT в заголовке Authorization, нужна ли защита от CSRF?
Если токен передаётся только в заголовке и не хранится в cookie, классический CSRF не работает. Но если JWT лежит в cookie, защита нужна. А вот хранение токена в localStorage делает его уязвимым при XSS.
Достаточно ли SameSite=Lax?
Для многих приложений это хорошая базовая защита, но для критичных операций стоит использовать токены и дополнительные проверки (defense in depth).
Что важнее: защита от XSS или от CSRF?
Нельзя выбирать что-то одно: XSS обходит защиту от CSRF, поэтому его устранение приоритетно, но CSRF-защита тоже обязательна.
Заключение
XSS и CSRF это не экзотика, а базовая гигиена веб-разработки. Обе уязвимости известны десятилетиями, обе хорошо изучены, и обе предотвращаются сравнительно простыми правилами:
- не доверяйте данным извне и экранируйте вывод с учётом контекста;
- используйте безопасные API (textContent вместо innerHTML, проверенные санитайзеры);
- защищайте изменяющие запросы токенами и SameSite-cookie;
- добавляйте слои защиты: CSP, HttpOnly, Secure, проверку Origin;
- не отключайте встроенные механизмы фреймворков;
- регулярно тестируйте своё приложение и обучайтесь на специальных тренировочных стендах.
Безопасность это не одна «волшебная настройка», а набор слоёв, и каждый из них закрывает свою часть рисков. Начните с чек-листа выше, и вы уже устраните большинство типичных проблем.
Источники и полезные ссылки
Теория и описание атак
- OWASP: Cross Site Scripting (XSS): https://owasp.org/www-community/attacks/xss/
- OWASP: Cross Site Request Forgery (CSRF): https://owasp.org/www-community/attacks/csrf
- OWASP Top 10: https://owasp.org/Top10/
- PortSwigger Web Security Academy: XSS: https://portswigger.net/web-security/cross-site-scripting
- PortSwigger Web Security Academy: CSRF: https://portswigger.net/web-security/csrf
Руководства по защите
- OWASP Cheat Sheet: XSS Prevention: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP Cheat Sheet: CSRF Prevention: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- MDN: Content Security Policy (CSP): https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- MDN: HTTP cookies (HttpOnly, Secure, SameSite): https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
- MDN: Same-origin policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
- web.dev: SameSite cookies explained: https://web.dev/articles/samesite-cookies-explained
- DOMPurify (библиотека санитизации HTML): https://github.com/cure53/DOMPurify
Тренировочные среды (для безопасной практики)
- OWASP Juice Shop: https://owasp.org/www-project-juice-shop/
- OWASP WebGoat: https://owasp.org/www-project-webgoat/
- PortSwigger Web Security Academy: https://portswigger.net/web-security
Исторический пример
- Samy (червь MySpace, 2005): https://en.wikipedia.org/wiki/Samy_(computer_worm)
Теперь с чего начать
Если вы ещё в самом начале пути и пока не решили, с какого фундамента начинать, то базовое понимание сетей, операционных систем, основ Linux и атак должно сформироваться до того, как вы откроете свой первый практический курс по пентесту.
Если вы хотите построить именно такой фундамент, начните с курса по кибербезопасности в JetSchool.

Автор
Джамал Ядигярлы
Специалист по Кибербезопасности

-1792420149186.webp&w=1920&q=64)
-1792158031796.webp&w=1920&q=64)
-1792156711857.webp&w=1920&q=64)
-1792156482750.webp&w=1920&q=64)

-1791547462132.webp&w=1920&q=64)