- Объяснять 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 ограничивает, откуда можно загружать скрипты.
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-строки не обойтись (например, шаблон на сервере), экранируй специальные символы: < превращается в <, & — в &, и браузер показывает их как текст, а не как теги.
function escapeHtml(text) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return String(text).replace(/[&<>"']/g, (char) => map[char]);
}
console.log(escapeHtml('<img src=x onerror="stealCookies()">'));
console.log(escapeHtml('Tom & Jerry say "hi"'));▸ Ожидаемый результат
<img src=x onerror="stealCookies()"> Tom & Jerry say "hi"
CSRF: межсайтовая подделка запроса
Пользователь вошёл на сайт своего банка, и cookie сессии лежит в браузере. Потом он открывает другой сайт, а тот отправляет скрытую форму на адрес банка /transfer. Браузер автоматически прикладывает cookie к запросу, и банк считает, что операцию сделал сам пользователь. Защита: cookie с атрибутом SameSite (Chrome считает cookie без него Lax), секретный случайный CSRF-токен в форме (сайт злоумышленника не может его прочитать), проверка заголовка Origin и правило никогда не менять данные GET-запросом.
Set-Cookie: session=8f3a2c91; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600| Атрибут | От чего защищает |
|---|---|
HttpOnly | JavaScript не может прочитать cookie — XSS не украдёт сессию |
Secure | cookie отправляется только по HTTPS |
SameSite=Lax | cookie не прикладывается к POST-запросам с других сайтов — CSRF сильно затруднён |
Max-Age | ограничивает время жизни сессии |
Проверяй данные, береги секреты
Не доверяй клиенту: всё, что приходит из браузера, — формы, URL, заголовки, JSON — можно подделать; злоумышленник обходит твой интерфейс и отправляет запросы прямо в API. Поэтому проверяй данные на сервере: тип, длину, формат и допустимые значения (список разрешённого, а не запрещённого). Проверка в браузере нужна только для удобства пользователя. Не собирай SQL-запросы склейкой строк — используй параметризованные запросы. В больших проектах помогают библиотеки схем (например, zod).
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.
// 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'] }],
},
},
];npx eslint . проверяет весь проект.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 . он выглядит как в блоке ниже.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Добавь tsconfig.json
С опциями
allowJsиcheckJs— TypeScript начинает проверять и существующие JS-файлы. - 2Пиши типы в JSDoc
Комментарии вроде
/** @param {number} price */добавляют типы без переименования файла. - 3Переименовывай файлы в
.tsпо одномуНачинай с самых простых модулей с малым числом зависимостей.
- 4Включи
strictВ конце
"strict": trueловит больше всего ошибок, например значения, которые могут бытьnull.
// 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): она заменяет &, <, >, " и ' соответственно на &, <, >, " и '. Достаточно одного replace и словаря замен.
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));
}▸ Ожидаемый результат
<b>Hello</b> Tom & Jerry <img src=x onerror="steal()">
Не допусти открытого перенаправления: isSafeRedirect(target) возвращает true только в двух случаях — относительный путь, который начинается с одного /, за которым не идёт ещё один / или \, либо адрес https://, у которого имя хоста ровно app.example.com.
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.