10 мин

Безопасность фронтенда

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.