Урок по треку Браузер и DOM 8 мин

Безопасность фронтенда: XSS, CSRF, CSP

XSS, CSRF, CSP, SameSite-куки, Trusted Types, кликджекинг и same-origin policy.

Фронтенд-разработчика на собесе почти всегда спрашивают про XSS, CSRF и CORS. Основа всего это same-origin policy.

Same-origin policy

Origin это схема + хост + порт. https://shop.ru и https://api.shop.ru разные origin, как и http://shop.ru и https://shop.ru.

Политика одного источника запрещает скрипту одного origin читать данные другого: ответы fetch, DOM чужого iframe, его хранилища. Отправить запрос на чужой сайт можно (форма, картинка), а прочитать ответ без разрешения через CORS нельзя.

Важно: site шире, чем origin. Site это схема + регистрируемый домен (shop.ru). api.shop.ru и shop.ru один site, но разные origin. На site опирается SameSite у кук.

XSS (межсайтовый скриптинг)

Атакующий добивается, чтобы его скрипт выполнился на вашем сайте. Скрипт получает всё, что есть у страницы: DOM, неHttpOnly-куки, токены в localStorage, может делать запросы от имени пользователя.

Виды:

  • Хранимая: вредный текст сохранён в базе (комментарий) и показывается всем.
  • Отражённая: вредный кусок приходит в URL и сервер вставляет его в ответ.
  • DOM-based: клиентский код сам берёт данные из location.hash или postMessage и вставляет в опасное место.
// уязвимо
el.innerHTML = `Привет, ${new URLSearchParams(location.search).get('name')}`;
// ?name=<img src=x onerror=alert(document.cookie)>

// безопасно
el.textContent = `Привет, ${name}`;

Защита:

  • Экранировать вывод по контексту. React делает это для {value} автоматически, но не для dangerouslySetInnerHTML и не для href="javascript:...".
  • Если HTML нужен, чистить его санитайзером (DOMPurify).
  • Опасные «приёмники» (sinks): innerHTML, outerHTML, document.write, eval, new Function, setTimeout со строкой, href и src с пользовательским URL.
  • CSP как второй рубеж.
  • Сессионные куки с HttpOnly, чтобы скрипт их не прочитал.

CSP (Content Security Policy)

Заголовок ответа, который говорит браузеру, откуда разрешено грузить и выполнять ресурсы.

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
  • Без 'unsafe-inline' инлайн-скрипты и onclick="..." не выполнятся, а значит внедрённый <script> тоже.
  • nonce: случайное значение на каждый ответ, <script nonce="r4nd0m"> выполнится, чужой без nonce нет.
  • 'strict-dynamic': скрипты, загруженные доверенным скриптом, тоже доверенные. Современная «строгая CSP» это nonce + strict-dynamic.
  • frame-ancestors запрещает встраивать сайт в чужие iframe (защита от кликджекинга, современная замена X-Frame-Options).
  • Content-Security-Policy-Report-Only для обкатки без блокировки.

Trusted Types

Механизм, который запрещает передавать обычные строки в опасные DOM-приёмники вроде innerHTML. Включается через CSP: require-trusted-types-for 'script'. После этого el.innerHTML = str бросит ошибку, принимается только объект TrustedHTML, созданный через вашу политику (например, с DOMPurify внутри). Так все места вставки HTML проходят через одну проверяемую точку. В Chromium-браузерах Trusted Types есть давно, в Safari и Firefox поддержка появилась недавно, поэтому перед включением сверяйтесь с caniuse и держите отчётный режим.

CSRF (межсайтовая подделка запроса)

Пользователь залогинен в bank.ru, заходит на evil.ru, а там скрытая форма отправляет POST bank.ru/transfer. Браузер прикладывает куки банка, и сервер думает, что это сам пользователь. Атакующий ответ не читает, ему достаточно, что действие выполнилось.

Защита:

  • SameSite у сессионной куки. Lax (по умолчанию в Chrome и Edge, если атрибут не указан) не отправляет куку в межсайтовых POST, iframe и fetch, но отправляет при переходе по ссылке верхнего уровня. Strict не отправляет ни в каком межсайтовом запросе. None отправляет всегда и требует Secure.
  • CSRF-токен: случайное значение в форме или заголовке, которое сервер сверяет с сессией. Чужой сайт его не знает.
  • Проверка заголовков Origin или Sec-Fetch-Site.
  • Не менять состояние через GET.

Если токен лежит в localStorage и отправляется в Authorization, CSRF не страшен (браузер сам его не приложит), зато XSS сразу его украдёт.

Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax; Path=/

Кликджекинг

Ваш сайт грузят в прозрачный iframe поверх приманки, пользователь кликает «Выиграть приз», а на деле жмёт «Удалить аккаунт». Защита: frame-ancestors 'none' в CSP или X-Frame-Options: DENY.

Ещё по мелочи

  • target="_blank": современные браузеры по умолчанию ставят noopener, но явный rel="noopener noreferrer" не помешает.
  • postMessage: всегда проверять event.origin у входящих сообщений и указывать целевой origin при отправке, а не '*'.
  • Подключая скрипт с CDN, используйте integrity (Subresource Integrity).
  • Секреты (ключи API) во фронтенд-бандле видны всем.

Частые ошибки

  • Думать, что CORS защищает от CSRF. CORS ограничивает чтение ответа, а запрос формы уйдёт и так.
  • Считать React полностью защищённым от XSS.
  • Хранить сессию в localStorage «чтобы не было CSRF» и забыть про XSS.
  • Путать site и origin.

Что спросят на собесе

  • Что такое XSS, какие бывают виды и как защищаться.
  • Как работает CSRF и почему SameSite помогает.
  • Что такое CSP и зачем nonce.
  • Где хранить токен: cookie или localStorage.
  • Что такое same-origin policy и чем site отличается от origin.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.