Ваш код под прицелом: почему безопасность TypeScript — это не опция, а необходимость
Вы когда-нибудь задумывались, что происходит, когда пользователь вводит в поле поиска на вашем сайте не просто «книги по Python», а нечто вроде '; DROP TABLE users; --? Если ваш бэкенд на TypeScript не готов к такому повороту, вы рискуете не просто получить ошибку, а потерять всю базу данных. Звучит как сценарий фильма ужасов? Добро пожаловать в реальный мир веб-разработки, где безопасность TypeScript — это не модная фишка, а базовая гигиена кода.
В этой статье мы не будем читать скучные лекции. Мы разберем двух главных врагов любого веб-приложения: SQL-инъекции и XSS (межсайтовый скриптинг). Вы узнаете, как хакеры атакуют ваши проекты, и, самое главное, как превратить ваш TypeScript-код в неприступную крепость с помощью реальных примеров и лучших практик.
SQL-инъекции: как злоумышленники «разговаривают» с вашей базой данных
Представьте, что ваше приложение — это вежливый консьерж, который выполняет любую просьбу гостя. Если гость просит: «Покажите комнату №5», консьерж показывает. Но что, если гость просит: «Покажите все комнаты и дайте мне ключи от сейфа»? Без проверки консьерж выполнит и это. SQL-инъекция работает именно так: злоумышленник «встраивает» свои команды в обычный пользовательский ввод.
Допустим, у вас есть простой запрос для поиска пользователя по имени:
// ❌ ОПАСНО: уязвимый код
const userName = req.body.username; // пользователь ввел "admin"
const query = `SELECT * FROM users WHERE name = '${userName}'`;
db.execute(query);
Выглядит безобидно? А теперь представьте, что пользователь вводит: admin' OR '1'='1. Запрос превращается в:
SELECT * FROM users WHERE name = 'admin' OR '1'='1'
Поздравляю! Вы только что отдали злоумышленнику список всех пользователей системы. А если он захочет что-то удалить? Легко: admin'; DROP TABLE users; -- — и вашей базы больше нет.
Как защититься: параметризованные запросы — ваш щит
Главное правило для защиты от SQL-инъекций в TypeScript: никогда не вставляйте пользовательские данные напрямую в строку запроса. Используйте параметризованные запросы (prepared statements). Они разделяют код SQL и данные, и база данных воспринимает пользовательский ввод исключительно как данные, а не как команды.
Пример с библиотекой pg (для PostgreSQL):
// ✅ БЕЗОПАСНО: параметризованный запрос
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
async function getUserByName(userName: string): Promise<any> {
const query = 'SELECT * FROM users WHERE name = $1';
const result = await pool.query(query, [userName]);
return result.rows;
}
// Использование
const userInput = req.body.username; // даже если это "admin' OR '1'='1"
const user = await getUserByName(userInput);
// База данных воспримет это как поиск пользователя с таким странным именем
console.log(user); // [] — ничего не найдено
Тот же принцип работает и с другими ORM, например, с Prisma или TypeORM. Они по умолчанию используют параметризованные запросы, что делает безопасность TypeScript встроенной функцией.
// ✅ БЕЗОПАСНО: использование Prisma
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function findUser(name: string) {
return prisma.user.findUnique({
where: { name: name } // Prisma сам экранирует входные данные
});
}
XSS-атаки: когда ваш сайт начинает транслировать чужой код
Если SQL-инъекции бьют по базе данных, то XSS (Cross-Site Scripting) атакует ваших пользователей. Представьте, что злоумышленник оставляет комментарий на форуме, но вместо текста вставляет JavaScript-код. Когда другой пользователь открывает страницу, этот код выполняется в его браузере. Результат? Кража cookies, перенаправление на фишинговые сайты или установка кейлоггера.
Классический пример уязвимости:
// ❌ ОПАСНО: отображение пользовательского ввода без обработки
function renderComment(comment: string): string {
return `<div class="comment">${comment}</div>`;
}
// Если пользователь ввел:
// <script>alert('XSS')</script>
// На странице появится всплывающее окно
Существует три основных типа XSS:
- Reflected (отраженный): вредоносный код передается через URL или форму и сразу же отображается на странице.
- Stored (хранимый): код сохраняется в базе данных (например, в комментарии) и выполняется при каждом просмотре страницы.
- DOM-based: атака происходит на стороне клиента через манипуляции с DOM.
Стратегия защиты: экранирование и Content Security Policy
Первая линия обороны — экранирование данных. Прежде чем вывести любой пользовательский контент, замените опасные символы на их HTML-сущности. В TypeScript для этого можно написать простую функцию:
// ✅ БЕЗОПАСНО: функция экранирования HTML
function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Использование
const userComment = "<script>alert('XSS')</script>";
const safeComment = escapeHtml(userComment);
console.log(safeComment); // <script>alert('XSS')</script>
Но если вы используете современные фреймворки (React, Angular, Vue), они уже делают это за вас. Например, React по умолчанию экранирует все данные, вставленные через JSX. Однако будьте осторожны с dangerouslySetInnerHTML — это прямой путь к уязвимости.
Вторая линия обороны — Content Security Policy (CSP). Это HTTP-заголовок, который говорит браузеру, какие ресурсы можно загружать и выполнять. Настройте CSP в вашем TypeScript-бэкенде:
// ✅ БЕЗОПАСНО: настройка CSP в Express.js
import express from 'express';
import helmet from 'helmet