Распознавай векторы XSS, специфичные для разметки, и применяй встроенную защиту HTML: sandbox, noopener и Content Security Policy.
Открыть этот урок в KodokonКонтекст экранирования решает всё: значение, безобидное в текстовом узле, становится исполняемым в атрибуте. Худший случай - атрибут без кавычек: достаточно одного пробела во внедрённых данных, чтобы создать новый атрибут, например обработчик onmouseover. Даже правильно взятое в кавычки, значение остаётся опасным в чувствительных приёмниках (sinks): href и src принимают схему javascript:, formaction перехватывает отправку формы, а srcdoc после декодирования интерпретирует свои сущности как HTML.
<!-- userName = x onmouseover=alert(1) -->
<img src="avatar.png" alt=x onmouseover=alert(1)>
<!-- The unquoted attribute lets a handler in -->Атрибут sandbox у iframe снимает все привилегии, и дальше ты возвращаешь их по одной: allow-scripts, allow-forms, allow-popups… Без allow-same-origin встроенный документ получает непрозрачное происхождение (opaque origin): ни куки, ни хранилища, ни доступа к DOM родителя - даже если он отдаётся с твоего собственного домена.
<iframe
src="https://widget.example.com"
sandbox="allow-scripts allow-forms"
referrerpolicy="no-referrer">
</iframe>Ссылка с target=_blank исторически давала открытой странице ссылку window.opener на твою вкладку: она могла перенаправить твою страницу на фишинговую копию - атака, известная как tabnabbing. Современные браузеры теперь применяют noopener неявно при target=_blank, но писать его явно по-прежнему необходимо для window.open в JavaScript и чтобы задокументировать намерение; добавь noreferrer, чтобы заодно убрать заголовок Referer.
<a href="https://external.example"
target="_blank"
rel="noopener noreferrer">
View the external resource
</a>Content Security Policy ограничивает источники исполняемого кода и обезвреживает большинство XSS, даже когда внедрение удалось. Объявленная через тег <meta http-equiv>, она тем не менее имеет слепые зоны: директивы frame-ancestors, report-uri и sandbox там игнорируются и требуют HTTP-заголовка. Предпочитай nonce, заново генерируемые при каждом ответе, спискам разрешённых доменов, которые директива strict-dynamic в любом случае делает устаревшими.
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-r4nd0m'">allow-scripts и allow-same-origin на iframe того же происхождения отменяет песочницу?rel=noopener?window.opener, чтобы перенаправить твою вкладку на фишинговый сайт<meta http-equiv>?