Безопасность TypeScript приложений: топ-10 уязвимостей

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Безопасность 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", может возникнуть                

Блоги

Рекомендаций по книгам