Введение: почему безопасность TypeScript приложений критична
TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию и современные возможности. Однако типизация не гарантирует безопасность. Ошибки валидации, неправильная обработка пользовательского ввода и уязвимости в зависимостях могут привести к серьёзным атакам. В этой статье мы разберём топ-10 наиболее частых уязвимостей TypeScript-приложений, приведём примеры кода и покажем, как их избежать.
1. Cross-Site Scripting (XSS)
XSS-атаки возникают, когда злоумышленник внедряет вредоносный скрипт в веб-страницу. TypeScript не защищает от этого автоматически.
Пример уязвимого кода
function renderUserInput(input: string): string {
return `<div>${input}</div>`;
}
// Злоумышленник передаёт: <script>alert('XSS')</script>
Решение
Используйте экранирование HTML-символов или библиотеки вроде DOMPurify:
import DOMPurify from 'dompurify';
function renderUserInputSafe(input: string): string {
const sanitized = DOMPurify.sanitize(input);
return `<div>${sanitized}</div>`;
}
2. SQL-инъекции
Если вы используете TypeScript с базами данных, неконтролируемая конкатенация строк в SQL-запросах может привести к утечке данных.
Пример уязвимого кода
async function getUser(id: string): Promise<User | null> {
const query = `SELECT * FROM users WHERE id = '${id}'`;
return await db.query(query);
}
Решение
Используйте параметризованные запросы (prepared statements):
async function getUserSafe(id: string): Promise<User | null> {
const query = 'SELECT * FROM users WHERE id = $1';
return await db.query(query, [id]);
}
3. Prototype Pollution
Эта уязвимость характерна для JavaScript/TypeScript. Злоумышленник может изменить прототип объекта, что повлияет на все объекты в runtime.
Пример уязвимого кода
function merge(target: any, source: any) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
const user = { name: 'Alice' };
merge(user, JSON.parse('{"__proto__": {"isAdmin": true}}'));
// Теперь все объекты имеют isAdmin: true
Решение
Проверяйте ключи на опасные свойства и используйте безопасные утилиты:
function safeMerge(target: Record<string, any>, source: Record<string, any>) {
for (const key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
target[key] = source[key];
}
}
4. Cross-Site Request Forgery (CSRF)
CSRF-атаки заставляют пользователя выполнить нежелательные действия на сайте, где он аутентифицирован.
Решение на TypeScript
// Генерация CSRF-токена
import crypto from 'crypto';
function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Проверка в middleware
function csrfMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers['x-csrf-token'];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
}
5. Небезопасная десериализация
Использование JSON.parse() или eval() на непроверенных данных может привести к выполнению произвольного кода.
Уязвимый пример
function processData(data: string) {
const obj = eval('(' + data + ')');
// ...
}
Безопасный подход
function processDataSafe(data: string) {
try {
const obj = JSON.parse(data);
// Дополнительная валидация типа
if (typeof obj !== 'object' || obj === null) throw new Error('Invalid input');
} catch {
throw new Error('Invalid JSON');
}
}
6. Инъекции через шаблонизаторы
Многие шаблонизаторы (например, Handlebars, EJS) могут выполнять код, если не экранировать ввод.
Решение
import Handlebars from 'handlebars';
// Включите экранирование по умолчанию
const template = Handlebars.compile('Hello {{name}}');
const output = template({ name: '<script>alert("XSS")</script>' });
// Handlebars автоматически экранирует: Hello <script>...
7. Утечка информации через стек-трейсы
В production-режиме стек-трейсы ошибок не должны содержать пути файлов или конфиденциальные данные.
// Плохо: глобальный обработчик ошибок выводит стек
process.on('uncaughtException', (err) => {
console.error(err.stack); // Утечка путей к файлам
});
// Хорошо: логируем только сообщение, стек — в защищённый лог
process.on('uncaughtException', (err) => {
logger.error(err.message);
// стек отправляется в защищённое хранилище
});
8. Небезопасные прямые ссылки на объекты (IDOR)
Если пользователь может получить доступ к чужим данным через изменение идентификатора в запросе.
Защита
// Вместо /api/users/:id — проверяйте права
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const currentUser = req.user; // из аутентификации
if (currentUser.role !== 'admin' && currentUser.id !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
// ...
});
9. Использование устаревших зависимостей
Многие уязвимости возникают из-за старых версий пакетов. Регулярно обновляйте зависимости.
// Проверка уязвимостей
npm audit
// Автоматическое обновление
npm update