Безопасность TypeScript приложений: топ-10 уязвимостей на практике
TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. Многие разработчики ошибочно полагают, что строгая типизация автоматически делает приложение безопасным. Это не так. Компилятор TypeScript проверяет типы, но не защищает от логических ошибок, инъекций или проблем с аутентификацией. В этой статье мы разберём 10 самых распространённых уязвимостей в TypeScript-приложениях, покажем примеры кода и дадим конкретные рекомендации по защите. Статья будет полезна как начинающим, так и опытным разработчикам.
1. Cross-Site Scripting (XSS) через неправильную обработку ввода
XSS остаётся одной из самых опасных уязвимостей. TypeScript не экранирует вывод автоматически. Если вы вставляете пользовательские данные в HTML-шаблон без санитизации, вы открываете дверь для атак.
Уязвимый код
function renderComment(comment: string): string {
return `<div>${comment}</div>`;
}
// Злоумышленник передаёт: <script>alert('XSS')</script>
Исправленный код
import { escape } from 'lodash';
function renderComment(comment: string): string {
const safeComment = escape(comment);
return `<div>${safeComment}</div>`;
}
Рекомендация: Всегда используйте библиотеки для экранирования (DOMPurify, lodash.escape) или шаблонизаторы с автоэкранированием (React JSX, Handlebars).
2. SQL-инъекции через строковые шаблоны
TypeScript позволяет использовать шаблонные строки для построения SQL-запросов. Это прямой путь к SQL-инъекции, если вы подставляете пользовательские данные.
Уязвимый код
async function getUser(id: string): Promise<User | null> {
const query = `SELECT * FROM users WHERE id = '${id}'`;
const result = await db.query(query);
return result.rows[0];
}
Исправленный код
async function getUser(id: string): Promise<User | null> {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.query(query, [id]);
return result.rows[0];
}
Рекомендация: Используйте параметризованные запросы или ORM (TypeORM, Prisma, Sequelize). Никогда не конкатенируйте строки для SQL.
3. Небезопасное сравнение паролей
Ошибка в реализации сравнения паролей может привести к timing-атакам. TypeScript-разработчики часто используют оператор === для сравнения хэшей, что небезопасно.
Уязвимый код
async function verifyPassword(
inputPassword: string,
storedHash: string
): Promise<boolean> {
const inputHash = await bcrypt.hash(inputPassword, 10);
return inputHash === storedHash; // Уязвимость: timing attack
}
Исправленный код
import bcrypt from 'bcrypt';
async function verifyPassword(
inputPassword: string,
storedHash: string
): Promise<boolean> {
return bcrypt.compare(inputPassword, storedHash);
}
Рекомендация: Используйте встроенные функции сравнения из библиотек хэширования (bcrypt.compare, argon2.verify). Они защищены от timing-атак.
4. Prototype Pollution через рекурсивное слияние объектов
TypeScript-приложения часто используют функции глубокого копирования или слияния объектов. Если не контролировать ключи, злоумышленник может загрязнить прототип Object.prototype.
Уязвимый код
function merge(target: Record<string, any>, source: Record<string, any>) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Злоумышленник передаёт: { "__proto__": { "isAdmin": true } }
Исправленный код
function safeMerge(target: Record<string, any>, source: Record<string, any>) {
for (const key in source) {
if (key === '__proto__' || key === 'constructor') continue;
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Рекомендация: Используйте проверенные библиотеки (lodash.merge с customizer), либо явно фильтруйте опасные ключи (__proto__, constructor, prototype).
5. Утечка данных через нестрогие типы (any)
Использование типа any в TypeScript полностью отключает проверку типов. Это может привести к утечке чувствительных данных, если вы случайно передаёте объект с лишними полями.
Уязвимый код
function getUserData(user: any): any {
return {
name: user.name,
email: user.email,
// Случайно включили passwordHash
passwordHash: user.passwordHash
};
}
Исправленный код
interface SafeUser {
name: string;
email: string;
}
function getUserData(user: SafeUser): SafeUser {
return {
name: user.name,
email: user.email
};
}
Рекомендация: Избегайте any. Используйте строгие интерфейсы и утилитарные типы (Pick, Omit, Partial) для контроля над данными.
6. CSRF-атаки через отсутствие токенов
TypeScript-приложения, особенно на Express или NestJS, часто забывают о защите от межсайтовой подделки запросов (CSRF).
Уязвимый код
app.post('/api/transfer', async (req, res) => {
const { toAccount, amount } = req.body;
await transferFunds(req.user.id, toAccount, amount);