惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
T
The Blog of Author Tim Ferriss
S
SegmentFault 最新的问题
量子位
V
Visual Studio Blog
F
Full Disclosure
博客园 - 叶小钗
Recent Announcements
Recent Announcements
G
Google Developers Blog
博客园 - Franky
F
Fortinet All Blogs
有赞技术团队
有赞技术团队
B
Blog
aimingoo的专栏
aimingoo的专栏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
Netflix TechBlog - Medium
L
LINUX DO - 最新话题
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
Cloudbric
Cloudbric
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Last Week in AI
Last Week in AI
SecWiki News
SecWiki News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
云风的 BLOG
云风的 BLOG
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
人人都是产品经理
人人都是产品经理
Schneier on Security
Schneier on Security
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Jina AI
Jina AI
PCI Perspectives
PCI Perspectives
Martin Fowler
Martin Fowler
O
OpenAI News
Hacker News: Ask HN
Hacker News: Ask HN
S
Secure Thoughts
P
Privacy International News Feed
I
InfoQ
C
Cyber Attacks, Cyber Crime and Cyber Security

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Безопасность приложений на Typescript от А до Я: гайд по защите от очевидных и не очень уязвимостей
poznohub · 2026-04-29 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение14 мин

Охват и читатели182

Обзор

Разберем вопросы защиты приложений на базе TypeScript

Разберем вопросы защиты приложений на базе TypeScript

Я часто замечаю, насколько некоторые разработчики халатно относятся к вопросам безопасности своих приложений. И начинают задумываться о методах защиты только тогда, когда уже приходится переписывать большую часть приложения. Сегодня мы пройдемся по классическим и не только методам атаки, посмотрим, где компилятор бессилен, и построим современную защиту, опираясь на лучшие практики и конкретные примеры кода.

В данной статье специально приведены упрощенные методы атак и примеры уязвимостей, чтобы было проще понимать саму механику.

Введение

Безусловно, TypeScript стал одним из лидеров в веб-разработке. На нем пишут мощные React-приложения, сложные микро-сервисы на Nest или fastify. В том числе, разработчики часто ценят безопасность типов, однако, это не классическая безопасность, ведь string в TypeScript - это всё ещё просто строка, которая может содержать и SQL-инъекции и XSS-уязвимости. Компилятор не проверяет бизнес-логику, не фильтрует входные данные и не видит, что вы отдали JWT-секрет в открытый репозиторий.

Эту статью я построил вокруг простого принципа: типы - это не защита, а инструмент дисциплины. Мы рассмотрим атаки и защитные механизмы на двух ключевых платформах:

  • Бэкенд (Node.js, Express/Fastify/NestJS): инъекции, прототипное загрязнение (Prototype Pollution), небезопасная десериализация, утечки данных через ошибки.

  • Фронтенд (React, Next.js, Angular): XSS, CSRF, отравление прототипов через зависимости, утечки чувствительных данных, атаки через SSR.

В каждом разделе я привёл реальные кейсы с кодом, простое объяснение уязвимости и способы устранения. Итак, впереди нас ждет увлекательное путешествие в мир защиты приложений.


Бэкенд: когда запрос приходит до проверки типов

TypeScript на сервере обеспечивает контракты между слоями, но входная точка в виде HTTP-запроса - это всегда сырые данные. Даже если вы используете NestJS с декораторами вроде @Body(), валидация может отсутствовать или быть неполной.

Кейс 1: SQL-инъекция через TypeORM (да, это возможно)

Как работают SQL инъекции

Как работают SQL инъекции

Многие думают, что ORM полностью защищает от инъекций. Но когда разработчик прибегает к сырым запросам или хитрым операторам, TypeScript не спасёт.

Уязвимый код (Базовый кейс с сырыми данными):

// Уязвимый эндпоинт на Express и TypeORM
import { getConnection } from 'typeorm';

app.get('/users', async (req, res) => {
  const { sortColumn, order } = req.query;
  
  // Ожидаем sortColumn = "name", order = "ASC"
  // И здесь уязвимость в небезопасном прямом запросе SQL
  const users = await getConnection().query(
    `SELECT * FROM users ORDER BY ${sortColumn} ${order}`
  );
  res.json(users);
});

Здесь параметры напрямую подставляются в SQL. Злоумышленник отправляет:

GET /users?sortColumn=name&order=ASC; DROP TABLE users; --

TypeScript видит sortColumn: string, всё хорошо с его точки зрения. Но реляционная база данных получает два запроса.

Решение: валидация разрешённых значений и использование параметризованных запросов или API, не допускающего конкатенации.

import { IsIn, IsString } from 'class-validator';
import { validateOrReject } from 'class-validator';

class UsersQueryDto {
  @IsIn(['name', 'email', 'createdAt'])
  sortColumn!: string;

  @IsIn(['ASC', 'DESC'])
  order!: 'ASC' | 'DESC';
}

app.get('/users', async (req, res) => {
  const dto = new UsersQueryDto();
  Object.assign(dto, req.query);
  await validateOrReject(dto);

  // Дальше можно использовать безопасный метод query builder
  const users = await userRepository.find({
    order: { [dto.sortColumn]: dto.order },
  });
});

Так мы гарантируем, что в ORDER BY не попадёт ничего кроме ожидаемых столбцов.

Казалось бы. Илья, что ты несёшь - мы и так пользуемся query builder, это очевидные вещи. Но я видел и такие решения, где разработчик подставлял частично сырые запросы. Например:

app.get('/search', async (req, res) => {
  const { q } = req.query;
  
  // Вроде бы используется query builder
  const users = await userRepository.find({
    where: {
      // Но здесь подставляется сырой запрос и создается уязвимость
      name: Raw(alias => `${alias} LIKE '%${q}%'`)
    }
  });
  res.json(users);
});

И получается, что здесь поисковая строка q напрямую вклеивается в SQL-выражение.

GET /search?q=%25'%3BDROP%20TABLE%20users%3B--

И если же всё таки вам никак не отказаться от Raw-вставок SQL кода - правильным решением будет: использовать параметризованные плейсхолдеры (поддерживаются, например, в TypeORM):

where: {
  name: Raw(alias => `${alias} ILIKE :query`, { query: `%${q}%` })
}

Другой похожий опасный паттерн: строить запрос через createQueryBuilder, склеивая строки для условий или сортировки.

app.get('/users', async (req, res) => {
  const { filter } = req.query; // filter = "admin'; DROP TABLE users; --"
  const qb = userRepository.createQueryBuilder('user');
  if (filter) {
    qb.where(`user.role = '${filter}'`);
  }
  const users = await qb.getMany();
  res.json(users);
});

При интерполяции строки внутри .where() открываются такие же возможности для инъекций, как и прямой SQL. Злоумышленник получает полный контроль над запросом.

Безопасная альтернатива: использовать параметры QueryBuilder:

if (filter) {
  qb.where('user.role = :role', { role: filter });
}

Ключевой урок: любая конкатенация строк при формировании SQL, подозрительна, даже если она спрятана за ORM-методами.


Кейс 2: NoSQL-инъекция в MongoDB с Mongoose

Даже при использовании ODM можно словить инъекцию, если передавать объекты из запроса напрямую.

// Уязвимый код
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  // req.body может содержать: { username: { $ne: null }, password: { $ne: null } }
  const user = await UserModel.findOne({ username, password }).exec();
  if (user) {
    res.json({ token: generateToken(user) });
  } else {
    res.status(401).send();
  }
});

Если клиент отправит JSON с операторами MongoDB ($gt$ne), то запрос превратится в { username: { $ne: null }, password: { $ne: null } } и вернёт первого попавшегося пользователя.

Решение: явная типизация и нормализация входных данных с помощью библиотек вроде mongo-sanitize или ручная проверка:

function sanitizeInput(obj: Record<string, unknown>): Record<string, string> {
  const clean: Record<string, string> = {};
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value !== 'string') {
      throw new Error('Invalid input type');
    }
    clean[key] = value;
  }
  return clean;
}

Но лучше использовать проверенные валидаторы, например Zod или class-validator, чтобы на уровне DTO запрещать объекты с подозрительными свойствами.


Повышаем планку. Кейс 3: Прототипное загрязнение (Prototype Pollution)

Пример прототипного загрязнения

Пример прототипного загрязнения

В Node.js объекты наследуют от Object.prototype, и изменение этого прототипа может привести к катастрофическим последствиям: от изменения логики до удалённого выполнения кода.

Пример такого кода - функция глубокого слияния:

// Наша опасная функция
function deepMerge(target: any, source: any) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

app.put('/settings', (req, res) => {
  const userSettings = JSON.parse(fs.readFileSync('settings.json', 'utf-8'));
  // Уязвимость
  deepMerge(userSettings, req.body);
  fs.writeFileSync('settings.json', JSON.stringify(userSettings));
  res.send('ok');
});

И если в запросе будет:

{ "__proto__": { "isAdmin": true } }

После такого слияния любой новый объект будет иметь isAdmin === true. Это может обойти проверки авторизации.

Защита: никогда не использовать рекурсивное слияние без проверки свойств. Современные библиотеки (lodash.merge) имеют защиту, но безопаснее будет вообще не использовать их для пользовательских данных. Лучше явно задавать схему:

import { z } from 'zod';

const SettingsSchema = z.object({
  theme: z.enum(['light', 'dark']),
  notifications: z.boolean(),
});

app.put('/settings', (req, res) => {
  const parsed = SettingsSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ errors: parsed.error });
  }
  
  // Дальнейшая работа только с parsed.data
});

Zod автоматически отбросит все неописанные ключи, включая proto и constructor.


Безопасная интеграция JWT и сессий

JWT стал стандартом индустрии, но зачастую, его неправильное использование приводит к краже токенов и повышению привилегий.

Кейс 4: Отсутствие проверки алгоритма

Рассмотрим уязвимый код:

import jwt from 'jsonwebtoken';

app.get('/profile', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).send();
  const decoded = jwt.verify(token, config.publicKey);
  // Атака: злоумышленник подписывает токен алгоритмом "none" или HS256 с публичным ключом
});

Если в библиотеке не зафиксирован допустимый алгоритм, можно использовать алгоритм none или симметричный алгоритм, зная публичный ключ.

Решение: явно указывать допустимые алгоритмы.

const decoded = jwt.verify(token, config.publicKey, {
  algorithms: ['RS256'], // или ['ES256']
});

В дополнение - никогда не использовать jwt.decode() для проверки. Только verify.


Кейс 5: Секреты в коде и конфигах

Случайно закоммитить .env файл с JWT_SECRET=super-secret в репозиторий - классика. TypeScript не сканирует содержимое строк. Используйте:

  • process.env и инструменты вроде dotenv-vault.

  • Валидацию конфигурации при старте через тот же Zod.

Проверка конфигураций через Zod:

const envSchema = z.object({
  JWT_SECRET: z.string().min(32),
  DB_URL: z.string().url(),
});
const env = envSchema.parse(process.env);

При нехватке/неправильной переменной приложение упадёт при запуске с ясной ошибкой.


Защита от SSTI (Server-Side Template Injection) в шаблонизаторах

Если вы отдаете рендеринг HTML на сервер (Nunjucks, EJS, Pug), неосторожная передача пользовательского ввода в шаблон может привести к выполнению кода.

Пример уязвимости:

app.get('/hello', (req, res) => {
  const name = req.query.name;
  res.render('hello', { name });
});
// Шаблон EJS: <h1>Hi <%= name %></h1>

Хотя <%= %> экранирует HTML, в некоторых движках можно внедрить исполняемый код через параметры шаблонизатора (как в случае с { constructor: ... }). Лучшая защита: никогда не передавать сырой ввод в шаблон без контекстной обработки и не включать продвинутые функции шаблонизатора (например, eval).

Если вы используете Next.js или React для SSR, аналогичная атака может проявляться через dangerouslySetInnerHTML:

function Profile({ bio }: { bio: string }) {
  return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}

Здесь TypeScript верит, что biostring, но переменная может содержать XSS.

Очевидное правило, которое видно даже из названия метода: никогда не использовать dangerouslySetInnerHTML с непроверенным пользовательским вводом, а если необходимо, применять DOMPurify.


Фронтенд: безопасность в браузере

На клиенте TypeScript даёт ложное чувство безопасности. Давайте рассмотрим основные векторы атак, где типы не помогут.

Кейс 6: XSS через вставку HTML

Как показано выше, передача неэкранированного текста в innerHTML или в JSX-атрибут dangerouslySetInnerHTML: прямой путь к XSS. Но есть менее очевидные места.

Небезопасный код в React:

function Comment({ text }: { text: string }) {
  return (
    <a href={`https://example.com/?q=${text}`}>
      Search
    </a>
  );
}
// Если text = "javascript:alert(1)"

Браузер выполнит JavaScript при клике. TypeScript не знает о контексте использования строки.

Защита: валидация URL и использование encodeURIComponent. Также не помешает Content Security Policy (CSP) со строгими директивами.


Кейс 7: Утечка конфиденциальных данных в сборку

Часто переменные окружения (API-ключи, внутренние URL) утекают в клиентский бандл, потому что разработчик использовал process.env.NEXT_PUBLIC_* или забыл о серверной / клиентской границе. TypeScript не различает, где будет выполняться код.

Защита: Чётко разделять env-переменные. В Next.js, например, только переменные с префиксом NEXT_PUBLIC_ доступны на клиенте. Всё остальное должно читаться только на сервере (getServerSideProps / API Routes).


Кейс 8: CSRF при мутациях

Если ваши куки передаются автоматически, а API принимает POST-запросы без дополнительной проверки, злоумышленник может заставить пользователя отправить нежелательный запрос.

TypeScript не добавит автоматически CSRF-токен. Нужно реализовывать либо синхронный токен, либо SameSite Cookie + проверку Origin/Referer.

Пример простой проверки в Next.js роутах API:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const allowedOrigins = ['https://myapp.com'];

export function middleware(req: NextRequest) {
  const origin = req.headers.get('origin');
  if (req.method !== 'GET' && (!origin || !allowedOrigins.includes(origin))) {
    return new NextResponse(null, { status: 403 });
  }
  return NextResponse.next();
}

Зависимости и supply chain

TypeScript-проекты тянут сотни пакетов. Каждая зависимость может стать точкой входа. Типизация не защищает от вредоносного кода в postinstall-скриптах или обфусцированном пакете.

Конкретный инцидент: event-stream

В 2018 году популярный npm-пакет event-stream был скомпрометирован: в него добавили вредоносный код, который воровал криптовалютные ключи из другого пакета. TypeScript здесь был бессилен: зловред может лежать глубоко в зависимостях и не содержать типов вовсе.

Защитные меры:

  • Использовать npm auditsnyksocket.dev.

  • Проверять лицензии и репутацию пакета.

  • Минимизировать количество зависимостей.

  • В CI/CD добавить этап проверки на известные уязвимости.


Типы как элемент защитной инфраструктуры

Несмотря на всё вышесказанное, TypeScript может существенно усилить безопасность, если его использовать осознанно:

  • Типизированные DTO и строгие интерфейсы. Применять не просто any, а точные типы, перечисления, discriminated unions. Это исключает множество ошибок валидации ещё на этапе написания кода.

  • Branded types (номинальная типизация). Например, мы можем создать тип SafeHtml, который можно получить только через функцию очистки.

  • Exhaustive switch и защита от неполноты. Гарантирует обработку всех возможных состояний (например, при разборе статусов аутентификации).

Пример защищенного типа SafeHtml:

type SafeHtml = string & { readonly __brand: unique symbol };

function sanitizeHtml(input: string): SafeHtml {
  return DOMPurify.sanitize(input) as SafeHtml;
}

function render(html: SafeHtml) {
  document.getElementById('app')!.innerHTML = html;
}

Повышаем уровень. Пять неочевидных современных атак на TypeScript приложения

Теперь переходим к угрозам, которые редко попадают в базовые гайды, но всё чаще встречаются в реальных проектах. Все примеры ориентированы на TypeScript-стек.

Dependency Confusion через типизированные пакеты

Злоумышленник публикует пакет с внутренним именем в публичном npm, но с более высокой версией. TypeScript-проекты особенно уязвимы из-за привычки использовать @types/* или корпоративные нейминги.

Пример: ваша компания использует внутренний пакет @mycompany/auth, который лежит в приватном реестре. Атакующий публикует @mycompany/auth в npm с версией 99.0.0 и вредоносным кодом в postinstall. Если в .npmrc не прописан строгий scope-реестр, npm install подтянет публичную версию.

// Код из вредоносного пакета (index.d.ts и index.js)
export function login(login: string, password: string): boolean;
// В JS: process.env.JWT_SECRET отправляется на сервер злоумышленника

Защита:

  • Настройте .npmrc с привязкой scope к приватному реестру.

  • Используйте npm install --prefer-offline и блокируйте запросы к публичному реестру для внутренних имён на уровне сети.

  • В CI пайплайне проверяйте целостность пакетов через npm audit --audit-level=high и сравнивайте хеши.


Timing-атака на сравнение строк (JWT, API-ключей)

Классическая ошибка: проверять токены или ключи через ===. В Node.js сравнение строк идёт побайтово и занимает разное время. Злоумышленник может измерить отклик и подобрать токен посимвольно.

Пример уязвимого кода:

const expectedApiKey = process.env.API_KEY!;
app.post('/webhook', (req, res) => {
  const apiKey = req.headers['x-api-key'] as string;
  if (apiKey !== expectedApiKey) {  // уязвимость
    return res.status(403).send('Forbidden');
  }
  // обработка
});

При неравных длинах сравнение обрывается мгновенно, а при правильном первом символе - чуть дольше. Повторяя запросы с разными значениями, можно восстановить ключ.

Защита: используйте crypto.timingSafeEqual для сравнения секретов.

import { timingSafeEqual } from 'crypto';

function constantTimeCompare(a: string, b: string): boolean {
  const bufA = Buffer.from(a);
  const bufB = Buffer.from(b);
  return timingSafeEqual(bufA, bufB);
}

И обязательно нормализуйте длину, чтобы пауза не выдавала длину ключа.


GraphQL: Introspection Abuse и инъекции в аргументы

На бэкенде с Apollo Server (TypeScript) часто оставляют включённой интроспекцию в production. Это позволяет злоумышленнику получить полную схему и найти секретные мутации или поля, доступные только админам. Ещё опаснее становится с инъекцией через невалидированные аргументы.

Уязвимость в резолвере:

const resolvers = {
  Query: {
    user: (_: unknown, args: { id: string }) => {
      // Аргумент id не проверяется на соответствие ожидаемому формату
      return db.raw(`SELECT * FROM users WHERE id = '${args.id}'`);
    }
  }
};

Шаги к защите:

  • Запретите интроспекцию в production.

  • Валидируйте аргументы через Zod или graphql-scalars.

Пример запрета интроспекции в конфигах:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
});

Пример валидации кода:

mport { z } from 'zod';
const userIdSchema = z.string().uuid();

user: (_: unknown, args: { id: string }) => {
  const id = userIdSchema.parse(args.id);
  return db.query('SELECT * FROM users WHERE id = $1', [id]);
}

SSRF через URL-парсинг в Node.js

Многие приложения принимают URL от пользователя (например, для импорта аватара). Злоумышленники обходят проверки с помощью Unicode-трюков или редиректов.

Пример уязвимого кода:

app.post('/import', async (req, res) => {
  const { url } = req.body as { url: string };
  const parsedUrl = new URL(url);
  if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {
    return res.status(400).send('Invalid URL');
  }
  const response = await fetch(url);
  // ...
});

Обход проверки хоста: http://127.0.0.1:80@evil.com (часть до @ считается учётными данными, в итоге hostname = evil.com, а запрос уходит на 127.0.0.1). Другой пример: http://0x7f.0.0.1/ (HEX-нотация IP).

Защита:

  • Не парсить URL самостоятельно. Используйте библиотеку вроде is-ip или проверяйте финальный IP после разрешения DNS.

  • Ограничьте схему только http и https. Запретите raw IP.

import { promises as dns } from 'dns';
async function resolveIp(url: string): Promise<string> {
  const hostname = new URL(url).hostname;
  const addresses = await dns.resolve4(hostname);
  return addresses[0]; // упрощённо
}

// Затем проверяйте на вхождение в приватные диапазоны 
// (10/8, 172.16/12, 192.168/16, 127/8)

RCE через небезопасную десериализацию в TypeScript

Некоторые библиотеки для удобства позволяют сериализовывать функции или выполнять eval при десериализации. Например, serialize-javascript (используется в Next.js) безопасен, но пакеты вроде node-serialize, cookie-serialize позволяют воспроизвести RCE.

Пример уязвимого кода:

import * as serialize from 'node-serialize';

app.get('/state', (req, res) => {
  const state = serialize.unserialize(req.cookies.state);
  // state может содерать объекты с кодом
});

Пример атаки: куки state с сериализованным объектом, где поле rce: "_$$ND_FUNC$$_function(){ require('child_process').exec('rm -rf /') }".

Защита: никогда не использовать десериализацию, которая может восстанавливать функции. Используйте только JSON. Например:

const state = JSON.parse(req.cookies.state || '{}');

Если нужны сложные типы, применяйте zod для валидации после JSON.parse, но не запускайте код. Любой импорт библиотек с расширенной сериализацией должен быть под запретом.


Практический чек-лист безопасности TypeScript-проекта

Для бэкенда:

  • Валидация всех входящих данных через Zod / class-validator / io-ts. Никаких any и as.

  • Параметризованные запросы к БД, никакой конкатенации строк (даже внутри Raw() и методах QueryBuilder).

  • Чистим объекты от proto и constructor (или используйте безопасные map/reduce).

  • Фиксированные алгоритмы JWT, короткое время жизни токенов, рефреш-токены с ротацией.

  • Безопасные настройки CORS (не * с credentials).

  • Логирование без утечки токенов/паролей.

  • Helmet-подобные middleware.

Для фронтенда:

  • Никакого dangerouslySetInnerHTML без DOMPurify.

  • CSP-заголовки, запрещающие inline-скрипты.

  • Правильное использование encodeURIComponent и валидация URL.

  • Разделение чувствительных env-переменных: в клиентский код попадает только то, что действительно нужно.

  • Защита от CSRF: SameSite=Strict/Lax, проверка Origin, токены для state-changing запросов.

  • Регулярное и очень внимательное обновление зависимостей.

Общие практики:

  • Линтер с правилами безопасности (eslint-plugin-security).

  • Статический анализ с тайпингами, но без фанатизма; помните, что any-кастование ломает защиту.

  • Runtime-проверки типов (ts-runtime, type guards) для данных с сервера, ведь ответ API тоже может быть не тем, что вы описали в интерфейсе.


Заключение

TypeScript, действительно мощный помощник, но не телохранитель. Строгая типизация снижает количество багов, делает код более предсказуемым, но не отменяет классические уязвимости веба. Сегодня мы разобрали реальные примеры, в которых компилятор абсолютно слеп к опасности: от подстановки в SQL (даже через высокоуровневые операторы TypeORM) до прототипного загрязнения, timing-атак и десериализации.

В дополнение, последние пять кейсов демонстрируют, что атаки мимикрируют под современные технологии, и защита должна эволюционировать.

Главный вывод: воспринимайте типы как фундамент, на котором вы строите многоуровневую систему безопасности. Валидируйте всё на границах доверия, никогда не доверяйте клиенту, и помните, что any это не тип, а дыра в защите.

Безопасность процесс, а не финальное состояние. Пусть ваш TypeScript будет не только строгим, но и безопасным.

Спасибо за прочтение. Какие еще виды уязвимостей вы хотели бы рассмотреть, возможно более глубоко и с неочевидных точек зрения?