Безопасность фронтенда
XSS, CSRF, CSP, авторизация на клиенте и безопасная работа с данными
- XSS
- CSRF
- CSP
- OAuth
Введение
Безопасность фронтенда — это не только «экранирование скобок». Это целый спектр угроз: от XSS-инъекций до атак на цепочку поставок (supply chain). На System Design интервью от вас ждут понимания основных векторов атаки и конкретных мер защиты. Фронтенд — первая линия обороны: он получает пользовательский ввод, хранит токены и отображает данные из недоверенных источников.
XSS — Cross-Site Scripting
XSS — самая распространённая уязвимость фронтенда. Атакующий внедряет вредоносный JavaScript, который выполняется в контексте вашего сайта с доступом к cookies, localStorage и DOM.
Типы XSS
- Reflected (отражённый) — вредоносный код в URL параметре, отображается на странице без санитизации. Пример:
search?q=<script>alert(1)</script>. - Stored (хранимый) — вредоносный код сохраняется в базе (комментарий, пост) и отображается всем пользователям. Самый опасный тип.
- DOM-based — код не проходит через сервер. JavaScript читает данные из URL/cookie и вставляет в DOM без экранирования. Пример:
document.getElementById("output").innerHTML = location.hash.
Защита от XSS
- React по умолчанию экранирует — JSX автоматически экранирует HTML-сущности.
<div>{userInput}</div>безопасен. НоdangerouslySetInnerHTMLопасен — используйте только с санитизированным HTML. - Санитизация HTML — если нужно рендерить пользовательский HTML (rich text editor, markdown), используйте
DOMPurify:
import DOMPurify from "dompurify";
const cleanHTML = DOMPurify.sanitize(userProvidedHTML, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "li"],
ALLOWED_ATTR: ["href", "target"],
});
- Content Security Policy (CSP) — HTTP-заголовок, запрещающий выполнение inline-скриптов и загрузку ресурсов с недоверенных доменов (подробнее — в разделе CSP).
- Trusted Types API — браузерный API, который запрещает присваивать строки потенциально опасным sink-ам (innerHTML, eval, src). Принудительно используйте policy для создания «доверенных» значений.
CSRF — Cross-Site Request Forgery
Атакующий создаёт страницу, которая выполняет запрос на ваш API от имени авторизованного пользователя. Браузер автоматически отправляет cookie с запросом — и сервер считает его легитимным.
Защита от CSRF
- SameSite cookies — атрибут
SameSite=StrictилиSameSite=Laxзапрещает отправку cookie с cross-origin запросов.Lax— рекомендуемый по умолчанию (разрешает GET-навигацию, блокирует POST). - CSRF-токен — сервер генерирует уникальный токен, фронтенд отправляет его в заголовке. Атакующий не может узнать токен с другого домена.
- Custom заголовки — добавление заголовка
X-Requested-With: XMLHttpRequestк запросам. Simple requests (GET/POST с определёнными content-types) не могут добавить custom заголовки — CORS preflight их заблокирует.
// Рекомендуемая настройка cookie для auth-токена
Set-Cookie: session=abc123;
HttpOnly; // не доступен из JavaScript
Secure; // только HTTPS
SameSite=Lax; // защита от CSRF
Path=/;
Max-Age=86400; // 24 часа
Content Security Policy (CSP)
CSP — мощнейший инструмент против XSS. Это HTTP-заголовок, который указывает браузеру, откуда разрешено загружать ресурсы:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.example.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Ключевые директивы:
script-src 'self' 'nonce-{random}'— запретить inline-скрипты, кроме имеющих правильный nonce. Блокирует большинство XSS.frame-ancestors 'none'— запретить встраивание в iframe (защита от clickjacking).connect-src— ограничить API-запросы только к вашим доменам.
В Next.js CSP настраивается через middleware:
// middleware.ts
import { NextResponse } from "next/server";
export function middleware(request: Request) {
const nonce = crypto.randomUUID();
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
`.replace(/\n/g, "");
const response = NextResponse.next();
response.headers.set("Content-Security-Policy", csp);
return response;
}
Аутентификация на клиенте
JWT vs Session — где хранить токен?
- httpOnly cookie — ✅ рекомендуемый подход. Токен недоступен из JS (защита от XSS), автоматически отправляется с запросами. Минус: нужна защита от CSRF (SameSite).
- localStorage — ❌ опасно. Доступен из любого JS на странице. Одна XSS-уязвимость — и токен украден. Единственное оправдание: SPA без бэкенда (pure CSR + third-party API).
- Memory (переменная) — ✅ безопасно, но не переживает перезагрузку. Хороший вариант для short-lived access token в комбинации с httpOnly refresh token cookie.
Паттерн: Access + Refresh
// Access token: короткоживущий (15 мин), в памяти
let accessToken: string | null = null;
// Refresh token: долгоживущий (30 дней), httpOnly cookie
// Автоматически обновляет access token при 401
async function fetchWithAuth(url: string, options?: RequestInit) {
let response = await fetch(url, {
...options,
headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
});
if (response.status === 401) {
const refreshed = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include", // отправить httpOnly cookie
});
const data = await refreshed.json();
accessToken = data.accessToken;
response = await fetch(url, {
...options,
headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
});
}
return response;
}
Input Validation на клиенте
Валидация на клиенте — это UX, а не безопасность. Серверная валидация обязательна всегда. Но клиентская валидация сокращает количество запросов и улучшает опыт пользователя.
- Используйте Zod или Yup для схемной валидации. Одна схема используется и на клиенте, и на сервере (в Next.js Server Actions).
- Ограничивайте длину ввода (maxlength), тип символов (pattern), формат (email, URL).
- Никогда не доверяйте данным из URL, localStorage, postMessage — всё это контролируется пользователем.
Supply Chain Attacks
Атаки на зависимости — растущая угроза. Вредоносный код в npm-пакете получает доступ ко всему, что доступно вашему приложению.
- Lockfile — всегда коммитьте
package-lock.json/pnpm-lock.yaml. Это фиксирует версии и хэши пакетов. - npm audit — регулярно запускайте для обнаружения известных уязвимостей.
- Dependabot / Renovate — автоматические PR на обновление зависимостей.
- Минимизируйте зависимости — каждая зависимость — это поверхность атаки. Не добавляйте пакет для одной функции, которую можно написать за 10 строк.
- Subresource Integrity (SRI) — для скриптов с CDN добавляйте
integrityхэш:<script src="..." integrity="sha384-...">
Checklist для интервью
- XSS: React экранирует по умолчанию, DOMPurify для пользовательского HTML, CSP для глобальной защиты
- CSRF: SameSite cookies + CSRF-токен для критичных мутаций
- Auth: httpOnly cookie для токенов, НИКОГДА localStorage
- CSP: ограничить script-src, connect-src, frame-ancestors
- Dependencies: lockfile, audit, минимизация, SRI
Принцип defense in depth: не полагайтесь на одну меру защиты. React экранирует XSS? Отлично, но добавьте CSP — это второй уровень защиты. SameSite cookie защищает от CSRF? Хорошо, но добавьте CSRF-токен для мутаций — это belt and suspenders.