Перейти к содержанию
Educora
Продвинутый24 мин27 / 27

Безопасность и лучшие практики

Защити пользователей и код: XSS и CSRF, проверка входных данных, секреты вне браузера, ESLint и Prettier и пошаговый переход на TypeScript.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять XSS и CSRF и предотвращать их с помощью textContent, экранирования, CSP, cookie с SameSite и CSRF-токенов
  • Проверять данные на сервере по спискам разрешённого и держать секреты вне клиентского кода и Git
  • Настраивать ESLint и Prettier и постепенно переводить проект на TypeScript

На твоём сайте есть поле для комментариев. Кто-то оставляет комментарий <img src=x onerror="stealCookies()">. Если твой код добавляет комментарии через innerHTML, этот скрипт выполнится в браузере каждого посетителя — с его сессией. В OWASP Top 10, списке самых серьёзных веб-рисков, есть атаки внедрения (injection), и XSS относится к ним. Безопасность — не функция, которую добавляют в конце, а набор повседневных привычек.

XSS: межсайтовый скриптинг

XSS возникает, когда недоверенные данные вставляются на страницу как HTML и скрипт злоумышленника работает от имени твоего сайта: крадёт cookie, совершает действия от лица пользователя. Защита: выводи пользовательский текст через textContent; если HTML действительно нужен, очищай его библиотекой вроде DOMPurify; React сам экранирует {value} (кроме dangerouslySetInnerHTML); заголовок Content-Security-Policy ограничивает, откуда можно загружать скрипты.

JavaScript
const comment = '<img src="x" onerror="stealCookies()">';

// dangerous: the text is parsed as HTML and onerror runs
list.innerHTML += `<li>${comment}</li>`;

// safe: the text stays text
const item = document.createElement('li');
item.textContent = comment;
list.append(item);
В безопасном варианте пользователь просто видит на экране текст <img ...>, и ничего не выполняется.

Если без HTML-строки не обойтись (например, шаблон на сервере), экранируй специальные символы: < превращается в &lt;, & — в &amp;, и браузер показывает их как текст, а не как теги.

JavaScript
function escapeHtml(text) {
  const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
  return String(text).replace(/[&<>"']/g, (char) => map[char]);
}

console.log(escapeHtml('<img src=x onerror="stealCookies()">'));
console.log(escapeHtml('Tom & Jerry say "hi"'));
▸ Ожидаемый результат
&lt;img src=x onerror=&quot;stealCookies()&quot;&gt;
Tom &amp; Jerry say &quot;hi&quot;

CSRF: межсайтовая подделка запроса

Пользователь вошёл на сайт своего банка, и cookie сессии лежит в браузере. Потом он открывает другой сайт, а тот отправляет скрытую форму на адрес банка /transfer. Браузер автоматически прикладывает cookie к запросу, и банк считает, что операцию сделал сам пользователь. Защита: cookie с атрибутом SameSite (Chrome считает cookie без него Lax), секретный случайный CSRF-токен в форме (сайт злоумышленника не может его прочитать), проверка заголовка Origin и правило никогда не менять данные GET-запросом.

Text
Set-Cookie: session=8f3a2c91; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
АтрибутОт чего защищает
HttpOnlyJavaScript не может прочитать cookie — XSS не украдёт сессию
Securecookie отправляется только по HTTPS
SameSite=Laxcookie не прикладывается к POST-запросам с других сайтов — CSRF сильно затруднён
Max-Ageограничивает время жизни сессии

Проверяй данные, береги секреты

Не доверяй клиенту: всё, что приходит из браузера, — формы, URL, заголовки, JSON — можно подделать; злоумышленник обходит твой интерфейс и отправляет запросы прямо в API. Поэтому проверяй данные на сервере: тип, длину, формат и допустимые значения (список разрешённого, а не запрещённого). Проверка в браузере нужна только для удобства пользователя. Не собирай SQL-запросы склейкой строк — используй параметризованные запросы. В больших проектах помогают библиотеки схем (например, zod).

JavaScript
function validateOrder(input) {
  const errors = [];
  const allowedSizes = ['S', 'M', 'L'];

  if (typeof input.email !== 'string' || !/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(input.email)) {
    errors.push('email: invalid format');
  }
  if (!Number.isInteger(input.quantity) || input.quantity < 1 || input.quantity > 10) {
    errors.push('quantity: must be an integer 1-10');
  }
  if (!allowedSizes.includes(input.size)) {
    errors.push(`size: must be one of ${allowedSizes.join(', ')}`);
  }
  return errors.length ? { ok: false, errors } : { ok: true };
}

console.log(validateOrder({ email: 'leyla@mail.az', quantity: 2, size: 'M' }));
console.log(validateOrder({ email: 'not-an-email', quantity: '100', size: 'XXL' }));
▸ Ожидаемый результат
{ ok: true }
{ ok: false, errors: [ 'email: invalid format', 'quantity: must be an integer 1-10', 'size: must be one of S, M, L' ] }
'100' — строка, поэтому Number.isInteger её отвергает: тип тоже нужно проверять. Возврат всех ошибок сразу помогает пользователю быстро исправить форму.

Секреты (ключи API, пароли, токены) становятся публичными, как только попадают в код браузера, — любой пользователь увидит их в DevTools. Платный API должен вызывать твой сервер, а браузер общается только с твоим сервером. Храни секреты в файле .env и добавь его в .gitignore. В Vite переменные с префиксом VITE_ встраиваются в клиентский код — значит, они публичны.

Качество кода: ESLint, Prettier, TypeScript

ESLint находит ошибки и плохие приёмы без запуска кода: неиспользуемые переменные, ==, необъявленные имена. Prettier автоматически форматирует код, поэтому команда не спорит о стиле. Запускай оба при сохранении в редакторе и в CI. Чтобы добавить ESLint в проект, достаточно npm init @eslint/config@latest, для Prettier — npm install -D prettier.

JavaScript
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';

export default [
  js.configs.recommended,
  {
    languageOptions: { globals: globals.browser },
    rules: {
      eqeqeq: 'error',
      'no-unused-vars': 'warn',
      'no-console': ['warn', { allow: ['warn', 'error'] }],
    },
  },
];
Современный файл «flat config» для ESLint: рекомендуемые правила плюс три своих. npx eslint . проверяет весь проект.
JavaScript
const user={name:"Aysel",age:15,city:'Baku'}
function greet(u){return "Hi, "+u.name}
if(user.age>=14){console.log(greet(user))}
Неотформатированный код. После npx prettier --write --single-quote . он выглядит как в блоке ниже.
JavaScript
const user = { name: 'Aysel', age: 15, city: 'Baku' };
function greet(u) {
  return 'Hi, ' + u.name;
}
if (user.age >= 14) {
  console.log(greet(user));
}

TypeScript добавляет в JavaScript статические типы: ошибки видны в редакторе ещё до запуска кода. Переводить большой проект за один день не нужно — переход делают постепенно:

  1. 1
    Добавь tsconfig.json

    С опциями allowJs и checkJs — TypeScript начинает проверять и существующие JS-файлы.

  2. 2
    Пиши типы в JSDoc

    Комментарии вроде /** @param {number} price */ добавляют типы без переименования файла.

  3. 3
    Переименовывай файлы в .ts по одному

    Начинай с самых простых модулей с малым числом зависимостей.

  4. 4
    Включи strict

    В конце "strict": true ловит больше всего ошибок, например значения, которые могут быть null.

TypeScript
// shop.ts
interface Product {
  id: number;
  title: string;
  price: number;
}

function total(products: Product[]): number {
  return products.reduce((sum, p) => sum + p.price, 0);
}

total([{ id: 1, title: 'Pen', price: '2' }]);
total([{ id: 2, title: 'Notebook' }]);
Ожидаемый результат
shop.ts(12,31): error TS2322: Type 'string' is not assignable to type 'number'.
shop.ts(13,8): error TS2741: Property 'price' is missing in type '{ id: number; title: string; }' but required in type 'Product'.
Запуск npx tsc --noEmit: обе ошибки найдены без запуска кода. В JavaScript '2' молча привело бы к склейке строк, а отсутствующий price — к NaN.
Задание

Напиши escapeHtml(text): она заменяет &, <, >, " и ' соответственно на &amp;, &lt;, &gt;, &quot; и &#39;. Достаточно одного replace и словаря замен.

Задание · JavaScript
function escapeHtml(text) {
  // replace & < > " ' with HTML entities
}

const comments = ['<b>Hello</b>', 'Tom & Jerry', '<img src=x onerror="steal()">'];
for (const comment of comments) {
  console.log(escapeHtml(comment));
}
▸ Ожидаемый результат
&lt;b&gt;Hello&lt;/b&gt;
Tom &amp; Jerry
&lt;img src=x onerror=&quot;steal()&quot;&gt;
Задание

Не допусти открытого перенаправления: isSafeRedirect(target) возвращает true только в двух случаях — относительный путь, который начинается с одного /, за которым не идёт ещё один / или \, либо адрес https://, у которого имя хоста ровно app.example.com.

Задание · JavaScript
function isSafeRedirect(target) {
  // 1) relative path: /^\/(?![\/\\])/
  // 2) otherwise parse with new URL() inside try...catch
  return true;
}

const targets = [
  '/lessons/42',
  '//evil.example/login',
  '/\\evil.example',
  'https://app.example.com/profile',
  'https://app.example.com.evil.example/',
  'javascript:stealCookies()',
];
for (const target of targets) {
  console.log(`${target} -> ${isSafeRedirect(target)}`);
}
▸ Ожидаемый результат
/lessons/42 -> true
//evil.example/login -> false
/\evil.example -> false
https://app.example.com/profile -> true
https://app.example.com.evil.example/ -> false
javascript:stealCookies() -> false

Главное

  • XSS: никогда не вставляй пользовательские данные как HTML — используй textContent, экранирование или очистку и добавь Content-Security-Policy.
  • CSRF: cookie отправляются автоматически; защищают SameSite, CSRF-токены, проверка Origin и запрет изменений через GET.
  • Не доверяй клиенту: проверяй данные на сервере по списку разрешённого и используй параметризованные SQL-запросы.
  • Секреты живут только на сервере и в .env, указанном в .gitignore; утёкший ключ сразу отзывай.
  • ESLint находит ошибки, Prettier форматирует, а TypeScript ловит ошибки типов до запуска — переходи на него постепенно через checkJs.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какой способ вывести комментарий пользователя на страницу безопасен?