Безопасность React
React автоматически экранирует значения в JSX — но у него есть мощные инструменты (dangerouslySetInnerHTML, refs), которые требуют осознанного подхода. Статья разбирает границы защиты фреймворка: где React защищает по умолчанию, какие приёмы обходят эту защиту и как строить безопасный клиентский код.
Содержание
- Как React защищает от XSS по умолчанию
- dangerouslySetInnerHTML: двустороннее лезвие
- Динамические атрибуты и URL
- Refs и императивный DOM
- Хранение токенов и авторизация
- Чек-лист безопасности React
- Терминология
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Как React защищает от XSS по умолчанию
React автоматически экранирует значения, вставленные в JSX. Любые HTML-теги в строке будут отображены как текст, а не выполнены.
function Comment({ userInput }) {
return <div>{userInput}</div>;
}
Если userInput содержит "<script>alert('xss')<\/script>", в DOM попадёт экранированный текст:
<div><script>alert('xss')</script></div>
React экранирует и значения атрибутов: " → ", & → & и так далее. Значение становится безопасным для вставки в HTML.
Где защита заканчивается
React не проверяет семантику URL. Если в href попадает javascript:alert(1), React экранирует кавычки, но протокол останется валидным для браузера — при клике выполнится JavaScript. То же касается прямого доступа к DOM через refs и несанитизированных сторонних библиотек.
dangerouslySetInnerHTML: двустороннее лезвие
dangerouslySetInnerHTML — прямой аналог v-html во Vue. Он вставляет HTML как есть, без экранирования.
// ❌ Опасно без санитизации
<div dangerouslySetInnerHTML={{ __html: userHtml }} />
Правильное использование
import DOMPurify from 'dompurify';
const safeHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href'],
});
// ✅ Санитизированный HTML
<div dangerouslySetInnerHTML={{ __html: safeHtml }} />
Предупреждение в документации
React официально предупреждает: dangerouslySetInnerHTML можно использовать только для доверенного контента. Никогда не используйте его для пользовательского ввода без санитизации через DOMPurify или аналоги.
Динамические атрибуты и URL
React не валидирует протоколы URL. Это распространённый вектор:
// ❌ Опасно
<a href={userUrl}>link</a>
Если userUrl = "javascript:alert(document.cookie)", при клике выполнится JavaScript.
Безопасная ссылка
function SafeLink({ href, children }) {
const isSafe = href.startsWith('/') || /^https?:\/\//.test(href);
if (!isSafe) return <span>{children}</span>;
return (
<a href={href} rel="noopener noreferrer" target="_blank">
{children}
</a>
);
}
Для более строгой защиты используйте библиотеку санитизации URL или белый список разрешённых доменов.
target=“_blank” и rel=“noopener”
Всегда используйте rel="noopener noreferrer" для внешних ссылок. Это защищает от tabnabbing — атаки, при которой открытая вкладка может изменить window.opener.location родительской страницы.
Refs и императивный DOM
Когда вы получаете прямой доступ к DOM-элементу через ref, вы обходите защиту React.
// ❌ XSS, если content содержит вредоносный HTML
const elRef = useRef(null);
useEffect(() => {
elRef.current.innerHTML = content;
}, []);
Безопасная альтернатива:
const elRef = useRef(null);
useEffect(() => {
elRef.current.innerHTML = DOMPurify.sanitize(content);
}, []);
Лучше всего избегать прямой манипуляции innerHTML вообще: рендерите контент через JSX, а HTML-строки не превращайте в разметку без санитизации.
Хранение токенов и авторизация
Хранение токенов
// auth/access-token.ts
let accessToken: string | null = null;
export function setAccessToken(token: string | null) {
accessToken = token;
}
export function getAccessToken() {
return accessToken;
}
Токен в памяти защищён от XSS лучше, чем в localStorage, но теряется при обновлении страницы. Для долгоживущих сессий предпочтительнее HttpOnly cookies — они не доступны JavaScript вообще.
Fetch с авторизацией
export async function fetchWithAuth(url: string, options: RequestInit = {}) {
const token = getAccessToken();
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: token ? `Bearer ${token}` : '',
},
});
}
Клиентская проверка авторизации — лишь адаптация UI. Настоящую проверку прав выполняет сервер.
Чек-лист безопасности React
- Использовать JSX-интерполяцию
{}вместоdangerouslySetInnerHTMLдля пользовательского текста. - Санитизировать HTML перед использованием в
dangerouslySetInnerHTML. - Валидировать URL в
hrefиsrc. - Использовать
rel="noopener noreferrer"для внешних ссылок. - Избегать
innerHTMLчерез refs. - Хранить токены в памяти или HttpOnly cookies, а не в
localStorage.
Терминология
| Термин | Значение |
|---|---|
| dangerouslySetInnerHTML | Свойство React для вставки сырого HTML |
| ref | Ссылка на DOM-элемент в React |
| DOMPurify | Библиотека санитизации HTML |
| Tabnabbing | Атака через window.opener у открытой вкладки |
| HttpOnly cookie | Cookie, недоступная JavaScript и защищённая от XSS |
Ключевые тезисы для интервью
- React автоматически экранирует значения в JSX и атрибуты — прямая аналогия с интерполяциями
{{ }}во Vue;dangerouslySetInnerHTML— прямой эквивалентv-html, без DOMPurify — XSS-уязвимость. - React не проверяет протоколы URL:
javascript:alert(1)вhrefвыполнится при клике;rel="noopener noreferrer"дляtarget="_blank"защищает от tabnabbing черезwindow.opener. innerHTMLчерезrefобходит защиту фреймворка — избегайте прямой манипуляции DOM; токен сессии в памяти защищён от XSS лучше, чем вlocalStorage, а HttpOnly cookies — лучше памяти.
Заключение
React предоставляет хорошую базовую защиту от XSS через автоматическое экранирование JSX, но у него есть мощные инструменты — dangerouslySetInnerHTML и refs — которые требуют осознанного подхода. Санитизация через DOMPurify, валидация URL и аккуратное хранение токенов закрывают основные векторы. Для более глубокого понимания XSS смотрите статью XSS: анатомия атаки, а про безопасность Vue — Безопасность Vue.