Безопасность TypeScript приложений: топ-10 уязвимостей
TypeScript, как надстройка над JavaScript, наследует многие его уязвимости, добавляя при этом специфические риски, связанные с системой типов и компиляцией. Даже строгая типизация не гарантирует безопасность — ошибки в логике и небезопасные практики могут привести к серьезным последствиям. В этой статье мы разберем топ-10 уязвимостей TypeScript приложений, приведем примеры кода и дадим рекомендации по защите.
1. Cross-Site Scripting (XSS) через dangerouslySetInnerHTML
Одна из самых распространенных уязвимостей в React/TypeScript приложениях — вставка неэкранированного HTML через dangerouslySetInnerHTML.
// ❌ Уязвимый код
function UserComment({ comment }: { comment: string }) {
return <div dangerouslySetInnerHTML={{ __html: comment }} />;
}
// ✅ Безопасный вариант
import DOMPurify from 'dompurify';
function SafeComment({ comment }: { comment: string }) {
const sanitized = DOMPurify.sanitize(comment);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
2. SQL-инъекции через неэкранированные запросы
Использование шаблонных строк в SQL-запросах с пользовательским вводом — прямой путь к инъекции.
// ❌ Уязвимый код
async function getUser(id: string) {
const query = `SELECT * FROM users WHERE id = '${id}'`;
return db.execute(query);
}
// ✅ Безопасный вариант (использование параметризованных запросов)
async function safeGetUser(id: string) {
const query = 'SELECT * FROM users WHERE id = $1';
return db.execute(query, [id]);
}
3. Prototype Pollution через глубокое слияние объектов
TypeScript позволяет легко копировать объекты, но не проверяет цепочку прототипов.
// ❌ Уязвимый код
function merge(target: any, source: 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;
}
// ✅ Безопасный вариант
function safeMerge(target: Record<string, any>, source: Record<string, any>) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor') continue;
if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = safeMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
4. NoSQL-инъекции (MongoDB)
При работе с MongoDB передача объекта напрямую из запроса может привести к инъекции операторов $where или $regex.
// ❌ Уязвимый код
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
// Если передать { "$gt": "" }, можно обойти проверку
});
// ✅ Безопасный вариант
import { sanitize } from 'mongo-sanitize';
app.post('/login', async (req, res) => {
const cleanBody = sanitize(req.body);
const user = await User.findOne({
username: cleanBody.username,
password: cleanBody.password
});
});
5. Команды ОС через child_process
Исполнение пользовательского ввода через exec без экранирования — критическая уязвимость.
// ❌ Уязвимый код
import { exec } from 'child_process';
function runCommand(input: string) {
exec(`echo ${input}`, (err, stdout) => {
console.log(stdout);
});
}
// ✅ Безопасный вариант
import { execFile } from 'child_process';
function safeCommand(input: string) {
// Используем execFile с массивом аргументов
execFile('echo', [input], (err, stdout) => {
console.log(stdout);
});
}
6. Path Traversal при работе с файлами
Неэкранированные пути к файлам позволяют читать системные файлы.
// ❌ Уязвимый код
import { readFileSync } from 'fs';
function readFile(filename: string) {
return readFileSync(filename, 'utf-8');
}
// ✅ Безопасный вариант
import { resolve, normalize } from 'path';
function safeReadFile(filename: string) {
const baseDir = resolve(__dirname, '../uploads');
const fullPath = normalize(resolve(baseDir, filename));
if (!fullPath.startsWith(baseDir)) {
throw new Error('Access denied');
}
return readFileSync(fullPath, 'utf-8');
}
7. Небезопасное использование eval() и Function()
TypeScript не блокирует eval, и его использование с пользовательским вводом очень опасно.
// ❌ Уязвимый код
function calculate(expression: string) {
return eval(expression); // Может выполнить любой код
}
// ✅ Безопасный вариант
function safeCalculate(a: number, b: number, op: string) {
const operations = {
'+': (x: number, y: number) => x + y,
'-': (x: number, y: number) => x - y,
'*': (x: number, y: number) => x * y,
'/': (x: number, y: number) => x / y,
};
const fn = operations[op];
if (!fn) throw new Error('Invalid operation');
return fn(a, b);
}
8. Insecure Direct Object References (IDOR)
Отсутствие проверки прав доступа к объектам по ID.
// ❌ Уязвимый код
app.get('/api/user/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // Любой пользователь может получить данные любого другого
});
// ✅ Безопасный вариант
app.get('/api/user/:id', async (req, res) => {
const userId = req.user.id; // Из JWT или сессии
if (req.params.id !== userId && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await User.findById(req.params.id);
res.json(user);
});
9. Отсутствие валидации типов (Type Confusion)
Даже с TypeScript, данные из внешних источников (API, формы) могут не соответствовать типам.
// ❌ Уязвимый код
interface UserInput {
age: number;
name: string;
}
function processUser(input: UserInput) {
// Если age пришло как строка "NaN", может возникнуть