7 мин

Проверка React-кода требует не одного инструмента

Разбираем, как устроена проверка React-кода после генерации: сравниваем ESLint, Semgrep и CodeQL по ошибкам, потоку данных и зависимостям.

Проверка React-кода требует не одного инструмента

Сгенерированный React-код нельзя считать проверенным после зеленого ESLint. Линтер хорошо ловит локальные ошибки и нарушения правил, но не доказывает, что данные из URL не дошли до опасного HTML, а установленный пакет не содержит известную уязвимость. Один зеленый отчет часто означает лишь то, что команда задала инструменту слишком узкий вопрос.

Рабочая схема состоит из нескольких проверок. ESLint дает быстрый ответ во время редактирования, Semgrep ищет опасные конструкции и понятные команде потоки данных, CodeQL разбирает более длинные пути между источником и опасной операцией. Состояние зависимостей проверяет отдельный сканер по lock-файлу. Если нужно выбрать минимум, берите ESLint и Semgrep вместе с аудитом зависимостей. CodeQL добавляйте там, где цена пропущенной уязвимости оправдывает более тяжелый анализ.

Три инструмента отвечают на разные вопросы

ESLint, Semgrep и CodeQL нельзя честно поставить в один ряд и выбрать победителя по числу найденных предупреждений. У них разные модели анализа, скорость обратной связи и цена настройки. Сравнивать нужно не бренды, а вопросы, которые инструмент способен задать к коду.

ESLint в первую очередь применяет правила к синтаксическому дереву JavaScript или TypeScript. Он отлично видит неиспользуемую переменную, недостижимую ветку, сомнительную работу с промисом и нарушение правил хуков, если подключен подходящий плагин. Официальный справочник ESLint делит встроенные правила на возможные ошибки, предложения и оформление. Уже это говорит о границе продукта: основной набор не обещает полноценный поиск уязвимостей.

Semgrep сопоставляет код со структурными шаблонами, а taint-правила связывают источник недоверенных данных с опасным приемником. Такое правило читается как политика команды: значение из location.search нельзя передавать в dangerouslySetInnerHTML, пока оно не прошло через разрешенный очиститель. В бесплатном движке область анализа уже, чем в коммерческих режимах с межфайловым анализом, поэтому результат зависит от того, пересекает ли поток границу файла.

CodeQL строит базу данных программы и выполняет запросы к ней. Его сильная сторона проявляется, когда путь данных проходит через несколько функций и файлов, а опасная операция не стоит рядом с источником. Встроенный набор запросов для JavaScript и TypeScript включает клиентский XSS, перенаправления по управляемому URL, инъекции кода, утечки чувствительных данных в логи и другие классы проблем. За эту глубину приходится платить временем анализа и более сложной отладкой запросов.

Ни один из трех подходов сам по себе не отвечает на вопрос, есть ли известная CVE в точной версии транзитивного npm-пакета. Это четвертый вопрос, и его нужно задавать дереву зависимостей, а не исходникам компонентов.

ESLint должен останавливать дешевые ошибки первым

ESLint стоит запускать на каждом изменении, потому что он быстро отсекает ошибки, которые бессмысленно отправлять в более дорогой анализ. Для сгенерированного кода это особенно заметно: модель может оставить неиспользуемый импорт, нарушить порядок вызова хуков, забыть обработать отклоненный промис или применить небезопасное приведение типа. Такие дефекты создают шум при ревью и иногда скрывают реальную ошибку за десятками мелких предупреждений.

Базового eslint:recommended для React мало. Нужны правила React и хуков, а для TypeScript полезен анализ с информацией о типах. Без него линтер видит синтаксис, но не знает, что промис проигнорирован или значение с типом any проходит через доверенный интерфейс. Типовая конфигурация должна покрывать файлы приложения, тесты и скрипты сборки разными блоками, а сгенерированные каталоги и артефакты сборки следует исключить явно.

Минимальный порог для CI выглядит так:

export default [
  {
    files: ['src/**/*.{js,jsx,ts,tsx}'],
    rules: {
      'no-eval': 'error',
      'no-implied-eval': 'error',
      'no-unsafe-optional-chaining': 'error'
    }
  }
]

Этот фрагмент не делает ESLint сканером безопасности. Он запрещает несколько опасных и часто случайных конструкций, причем результат появляется прямо в редакторе. Правила хуков и TypeScript подключаются отдельными плагинами, версии которых нужно закрепить вместе с остальными инструментами проекта.

Популярный совет «включить все правила как ошибки» обычно вреден. После генерации он выдает длинный список претензий к стилю, и разработчик либо отключает проверку целиком, либо привыкает игнорировать красный статус. В обязательный набор должны входить ошибки выполнения, нарушения модели React и несколько запретов, связанных с принятой архитектурой. Оформление лучше чинить форматтером, а спорные рекомендации сначала оставить предупреждениями.

ESLint проще всех подключить: пакет уже есть в большинстве React-проектов, конфигурация хранится рядом с кодом, запуск занимает мало времени. Но локальное правило редко прослеживает значение через цепочку абстракций. Если оно сообщает о dangerouslySetInnerHTML, то обычно видит сам вызов, а не доказывает, что конкретная строка пришла от пользователя.

Semgrep хорошо фиксирует правила конкретного проекта

Semgrep полезен сразу после ESLint, когда команда знает опасные конструкции своего приложения и хочет выразить запрет коротким проверяемым правилом. Он понимает структуру языка, поэтому шаблон не ломается от переименования переменной или другого форматирования, как случилось бы с обычным поиском текста.

Для React разумно начать с мест, где браузер превращает строку в действие или разметку: dangerouslySetInnerHTML, присваивание innerHTML, создание URL для перехода, вызовы eval и похожих API, передача токена в журнал. Простое search-правило найдет сам приемник. Taint-режим полезнее, когда нужно отличить константную разметку от данных, взятых из адресной строки, хранилища, сообщения окна или ответа API.

Документация Semgrep описывает taint-анализ через четыре роли: источник, приемник, очиститель и распространитель. Такое разделение удобно для код-ревью, потому что правило можно обсуждать на языке архитектуры. Но очиститель нельзя объявлять по названию функции на веру. Если команда записала sanitizeHtml() в pattern-sanitizers, а функция лишь удаляет тег script, анализ даст ложное чувство безопасности: обработчики событий и опасные URL останутся.

Небольшое правило, запрещающее прямой HTML в JSX, можно хранить в репозитории:

rules:
  - id: react-no-raw-html
    languages: [typescript, javascript]
    severity: ERROR
    message: Проверить источник и очистку HTML
    patterns:
      - pattern: <$TAG dangerouslySetInnerHTML={{__html: $VALUE}} ... />

Ожидаемый результат имеет конкретную форму: идентификатор react-no-raw-html, путь к файлу, диапазон строк и сообщение. Если проверка не находит подготовленный тестовый пример, конфигурацию нельзя выпускать в CI. Для собственных правил нужны положительные и отрицательные тесты, иначе небольшое изменение JSX превратит защиту в молчащий файл YAML.

Сложность подключения Semgrep средняя. Запуск готового набора прост, собственное правило пишется быстрее запроса CodeQL, а находку легко объяснить по совпавшему фрагменту. Трудность начинается с межфайловых потоков, оберток над API и настройки исключений. Согласно глоссарию Semgrep, Community Edition ограничивает анализ одним файлом; это существенная оговорка для React-проекта, где чтение параметра, преобразование и рендер часто разнесены по модулям.

CodeQL находит длинные пути данных

CodeQL имеет смысл там, где опасность определяется не одной конструкцией, а связью между удаленными частями программы. Он превращает исходники в базу, где запрос описывает источники, приемники и допустимые шаги потока. Path query возвращает не только строку с проблемой, но и маршрут значения, который разработчик может пройти по шагам.

Представим, что компонент читает returnTo из URL, передает его в хук, хук кладет значение в объект состояния, утилита нормализует пробелы, а обработчик присваивает результат window.location. Простое правило на опасное присваивание создаст находки и для жестко заданных внутренних путей. Запрос с потоком данных способен связать управляемый параметр с перенаправлением и оставить безопасные константы в покое.

Официальная документация GitHub для path queries говорит, что каждый результат описывает поток от источника к приемнику, а интерфейс позволяет раскрыть отдельные шаги. Это полезнее одиночной строки, но не отменяет ручной проверки. Модель библиотеки может не знать вашу функцию нормализации, а собственный источник данных может отсутствовать в стандартном наборе. Тогда запрос пропустит путь или покажет его неточно.

Для обычного React-фронтенда стандартные запросы CodeQL дают больше пользы, чем попытка сразу писать QL самостоятельно. Начните с набора default, затем оцените security-extended на отдельной ветке. Расширенный набор способен добавить шум, поэтому его не стоит превращать в обязательный барьер до разбора существующих результатов.

Цена подключения выше, чем у ESLint и Semgrep. Локально нужно создать или получить базу, выбрать пакеты запросов и научиться читать трассу. В CI анализу требуются отдельные права, время и хранение результатов. На GitHub доступность встроенного сканирования зависит от типа репозитория и оплаченных возможностей организации. Сам CodeQL CLI можно применять отдельно при соблюдении его условий, но тогда команда сама организует запуск и выдачу результатов.

CodeQL не заменяет компилятор TypeScript, тесты или браузерные проверки. Он отвечает на вопросы, закодированные в запросах. Зеленый результат означает, что выбранные запросы не нашли известный им путь, а не то, что приложение безопасно при любом вводе.

Уязвимые зависимости требуют отдельного барьера

Российская среда для генерации
TakProsto создает приложения на российских серверах с локализованными моделями, а код можно экспортировать.

Небезопасную версию npm-пакета нужно искать по package-lock.json или другому lock-файлу отдельным инструментом. ESLint анализирует файлы программы, Semgrep Supply Chain относится к отдельному продукту, а CodeQL code scanning не равен проверке состава зависимостей. Смешение этих задач регулярно оставляет дыру в процессе: команда включает SAST и решает, что закрыла цепочку поставки.

Документация npm прямо говорит, что npm audit отправляет описание дерева зависимостей в настроенный реестр и получает сведения об известных уязвимостях. Для воспроизводимого результата нужен lock-файл. Команда без него может собрать дерево заново, и два запуска перестанут проверять один и тот же набор версий.

Для CI подходит команда:

npm audit --audit-level=high

Ненулевой код завершения блокирует сборку при находках заданного порога, но параметр audit-level не фильтрует сам отчет. Не запускайте npm audit fix --force автоматически в основной ветке. Исправление выполняет установку зависимостей, а флаг force может разрешить несовместимые обновления. Пусть бот или разработчик создаст отдельное изменение lock-файла, после чего тесты проверят поведение приложения.

Есть еще разница между уже установленной уязвимостью и новой уязвимой зависимостью в pull request. npm audit отвечает на первый вопрос для текущего дерева. GitHub Dependency Review сравнивает изменения манифестов и lock-файлов и может остановить добавление известной уязвимости. Документация GitHub отдельно отмечает транзитивные изменения. Это не функция CodeQL, хотя результаты могут появляться в том же интерфейсе безопасности.

Semgrep Supply Chain связывает сведения о зависимости с достижимостью уязвимого кода, но такое сопоставление нужно оценивать как отдельную возможность с собственными условиями. Нельзя переносить ее свойства на бесплатный запуск semgrep scan. При выборе любого сканера проверьте поддержку вашего менеджера пакетов, частного реестра, workspace-структуры и формат исключений с датой окончания.

Выбор зависит от цены пропуска

Для большинства команд выбор выглядит не как «Semgrep или CodeQL», а как последовательность проверок разной стоимости. Быстрая проверка должна срабатывать часто, глубокая может работать реже, если она охватывает то, чего не видят ранние ступени.

КритерийESLintSemgrepCodeQL
Локальные ошибки JavaScript и ReactСильный с подходящими плагинамиЧастичное покрытие правиламиНе основная задача
Опасные синтаксические шаблоныВозможны точечные запретыСильная сторонаВозможны запросы, но это дороже
Поток данных внутри файлаОграничен отдельными правиламиTaint-правилаПодробный анализ и трасса
Поток между файламиОбычно нетЗависит от режима продуктаСильная сторона
Известные уязвимости npm-пакетовНетОтдельный Supply ChainОтдельные средства dependency review
Скорость обратной связиСамая высокаяВысокая для типовых правилНиже из-за построения базы и запросов
Свои проверкиПлагин на JavaScriptКороткое YAML-правилоЗапрос на QL и модель библиотек

Если приложение показывает публичные данные и не обрабатывает учетные записи, ESLint, несколько правил Semgrep и аудит lock-файла обычно дают разумное начальное покрытие. Если фронтенд принимает платежные данные, работает с административными действиями, отображает пользовательский HTML или строит переходы по внешнему вводу, межфайловый анализ становится гораздо важнее.

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

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

Первый запуск почти всегда поднимает старый долг. Если сразу сделать каждую находку блокирующей, команда получит стену ошибок, не связанную с текущим изменением. Лучше сохранить проверенный снимок существующих результатов как базовую линию и блокировать новые нарушения. Базовая линия не списывает старые проблемы: назначьте владельца каждому опасному классу и разбирайте очередь отдельно. Инструмент должен сравнивать текущий результат с конкретным коммитом, иначе удаленная и заново появившаяся уязвимость легко пройдет как уже известная.

При одинаковой серьезности первой исправляйте находку с подтвержденным путем от внешнего ввода к действию в браузере. Затем смотрите, доступен ли уязвимый компонент обычному пользователю, нужна ли авторизация и есть ли реальный санитайзер. Название уровня high само по себе не заменяет эту проверку. Разные анализаторы по-разному назначают серьезность, поэтому единая очередь должна хранить идентификатор правила, место, путь данных и решение разработчика.

CI должен давать разработчику один понятный сигнал

Откат после неудачного автофикса
Верните проект к снимку, если массовое исправление ESLint или обновление пакетов сломало приложение.

Проверки стоит развести по времени и назначению, но собрать их результат в понятные статусы pull request. Разработчик должен видеть, какая политика нарушена, где лежит находка и кто может принять исключение. Без этого четыре сканера создают четыре очереди предупреждений, которые никто не считает общей ответственностью.

Практичная последовательность внедрения занимает четыре этапа:

  1. На каждом push запускайте TypeScript и ESLint только на проектных файлах. Ошибки выполнения и правила хуков блокируют слияние, оформление исправляет отдельная команда.
  2. В pull request запускайте Semgrep с закрепленным набором правил и npm audit по существующему lock-файлу. Собственные правила сначала проверяйте на небольших тестовых фрагментах.
  3. CodeQL запускайте для pull request или по расписанию, если полный анализ слишком долог. Начните со стандартного набора запросов и назначьте владельца очереди результатов.
  4. После периода наблюдения сделайте обязательными только стабильные проверки. Для исключения требуйте причину, владельца и срок пересмотра.

Закрепляйте версии инструментов и правил. Автоматическое получение самого нового набора в каждой сборке удобно до первого внезапного изменения, которое заблокирует все ветки утром. Обновление анализаторов должно приходить отдельным изменением, чтобы команда видела новые находки и могла скорректировать политику.

Храните результаты в формате, который показывает путь и правило, а не только счетчик. SARIF подходит для Semgrep и CodeQL, если ваша CI-платформа умеет его отображать. ESLint может выдавать машинный отчет отдельно от короткого консольного вывода. В журнале сборки оставляйте команду, версию анализатора и идентификатор набора правил, иначе воспроизвести вчерашнюю находку будет трудно.

Разделяйте сбой инструмента и найденную проблему. Если анализатор не смог скачать правила, построить базу или разобрать файл, CI не должен показывать зеленый статус. Такой запуск завершился без результата. Введите отдельный статус ошибки сканирования и сохраняйте число обработанных файлов. Резкое падение покрытия после изменения конфигурации опаснее десятка знакомых предупреждений, потому что создает тихую слепую зону.

Проверка только измененных файлов подходит ESLint, но требует осторожности для потока данных. Изменение общей утилиты способно сделать опасными десятки неизмененных компонентов. Semgrep с пофайловыми правилами можно запускать на diff для быстрой подсказки и полностью по расписанию. CodeQL должен видеть согласованную базу всего проекта, иначе запрос теряет связи между модулями. Экономить минуты CI за счет обрезанного графа данных обычно плохая сделка.

На проектах, выгруженных из TakProsto, исходный React-код доступен для таких проверок, а снимок и откат помогают безопасно вернуться к состоянию до неудачного исправления. Это не меняет порядок контроля: экспортированный код проходит те же линтеры, SAST и аудит зависимостей, что и написанный вручную.

Одна строка HTML показывает разницу подходов

React-код остается доступным для проверки
TakProsto экспортирует исходники, чтобы вы запускали ESLint, Semgrep и CodeQL в своем CI.

Сбой становится понятным, если проследить один реалистичный фрагмент через все проверки. Генератор получил требование показать форматированное описание товара и написал компонент:

const params = new URLSearchParams(window.location.search)
const preview = params.get('preview') ?? product.description
return <section dangerouslySetInnerHTML={{ __html: preview }} />

TypeScript принимает этот код: preview имеет тип string. ESLint с правилами возможных ошибок тоже может промолчать, потому что синтаксис корректен, хуки не нарушены, переменные используются. Правило, запрещающее любой dangerouslySetInnerHTML, найдет строку, но даст такое же предупреждение для константного доверенного шаблона.

Taint-правило Semgrep может объявить params.get(...) источником, значение __html приемником, а проверенную функцию очистки санитайзером. Тогда отчет объяснит, что пользовательский параметр дошел до HTML. Если чтение параметра вынести в один файл, преобразование в другой, а компонент в третий, результат бесплатного пофайлового режима уже нельзя считать гарантированным.

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

Проверка зависимостей в этом примере вообще не обязана сработать. Код уязвим даже при полностью чистом lock-файле. Обратная ситуация тоже обычна: компонент безопасно экранирует текст, но транзитивный пакет содержит известную уязвимость. Поэтому объединять SAST и аудит пакетов одним словом «безопасность» удобно для отчета, но бесполезно для исправления.

Правильное исправление зависит от требования. Если нужна обычная строка, React сам экранирует {preview}. Если продукт действительно принимает ограниченный HTML, команда выбирает проверенный очиститель, задает разрешенный набор тегов и атрибутов, тестирует опасные URL и обработчики событий, а затем учит анализатор распознавать именно этот путь. Простая замена одного API другим без теста оставляет неизвестность.

После исправления добавьте два теста. Первый передает безопасную разметку из разрешенного набора и проверяет ожидаемый результат. Второй передает строку с запрещенным обработчиком события или опасной схемой URL и проверяет, что она не выполняется. Затем оставьте уязвимый минимальный фрагмент в тестах правила Semgrep или запроса CodeQL. Тест приложения доказывает поведение очистителя, а тест анализатора доказывает, что будущий обход снова поднимет предупреждение. Эти проверки отвечают на разные вопросы, поэтому одна не заменяет другую.

Генерация меняет приоритет, а не стандарты

После генерации сначала проверяйте границы доверия, а не красоту компонентов. Модель хорошо воспроизводит знакомый шаблон, но не знает, какие данные ваша система считает доверенными, какие домены разрешены для перехода и что допустимо записывать в журнал. Эти решения должны жить в правилах, тестах и конфигурации CI, а не в очередном запросе к генератору.

Для нового React-проекта я ставлю обязательный ESLint с правилами React и TypeScript, Semgrep с небольшим набором запретов приложения и аудит зависимостей по lock-файлу. CodeQL включаю до выхода в эксплуатацию, если приложение обрабатывает пользовательский контент, секреты, административные действия или сложные перенаправления. В большом монорепозитории включаю его раньше, потому что границы файлов быстро обесценивают пофайловый анализ.

Не разрешайте генератору автоматически «исправить все предупреждения» одним большим изменением. Он может подавить правило, добавить сомнительный санитайзер или обновить основной пакет вместе с десятками транзитивных зависимостей. Каждая находка должна привести к небольшому изменению с тестом, а исключение должно объяснять, почему конкретный поток безопасен.

Просматривайте и сами конфигурационные изменения. Правка eslint.config.js, файла правил Semgrep, workflow CodeQL или lock-файла меняет границу контроля сильнее, чем обычный компонент. Для этих путей полезно требовать отдельного владельца ревью. Генератору легко добиться зеленого статуса удалением плагина, расширением списка игнорируемых каталогов или переводом ошибки в предупреждение. CI формально пройдет, хотя проверять станет меньше.

Полезная метрика процесса проста и не требует выдуманных процентов: для каждой обязательной проверки возьмите последнюю настоящую находку и попросите другого разработчика воспроизвести ее локально по журналу CI. Если он не может понять правило, путь данных или проверенную версию пакета, барьер пока оформлен плохо. Повторяйте такую проверку после обновления анализатора или набора правил. Зеленый статус ценен только тогда, когда красный статус приводит к понятному исправлению.

FAQ

Можно ли заменить Semgrep обычными правилами ESLint?

Только для простых локальных запретов. ESLint удобно ловит вызов опасного API, но Semgrep проще связывает источник данных с приемником и выражает такие политики в YAML. Для межфайловых потоков нужно отдельно проверить возможности выбранного режима.

Нужен ли CodeQL небольшому React-проекту?

Не всегда на первом этапе. Если компоненты простые, а пользовательские данные не доходят до опасных API, ESLint, Semgrep и аудит зависимостей дадут хороший минимум. CodeQL становится полезнее при сложных хуках, общих библиотеках и высокой цене пропущенной уязвимости.

Проверяет ли ESLint уязвимости npm-зависимостей?

Нет. ESLint анализирует исходные файлы по правилам и не сверяет установленное дерево пакетов с базой известных уязвимостей. Для этого нужен npm audit или другой сканер, который читает lock-файл.

Чем taint-анализ отличается от поиска опасной функции?

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

Достаточно ли запустить Semgrep с автоматическим набором правил?

Этого достаточно для первого обзора, но не для политики конкретного приложения. Автоматический набор не знает ваши обертки API, доверенные границы и разрешенные способы очистки. Добавьте несколько собственных правил и тесты к ним.

Что запускать первым после генерации React-кода?

Сначала TypeScript и ESLint, потому что они быстро убирают ошибки типов, хуков и выполнения. Затем запускайте Semgrep и проверку lock-файла. Глубокий CodeQL-анализ можно выполнять после быстрых барьеров или параллельно в CI.

Может ли CodeQL заменить тесты безопасности?

Нет. CodeQL находит пути, описанные запросами и моделями библиотек, но не исполняет интерфейс как браузер и не проверяет все свойства очистителя. Для опасного HTML, переходов и авторизации нужны отдельные тесты с вредоносным вводом.

Почему нельзя автоматически выполнять npm audit fix?

Команда меняет дерево пакетов, а с --force может принять несовместимое обновление. Такое исправление нужно делать отдельным изменением lock-файла и прогонять через тесты. Автоматически блокировать известную уязвимость безопаснее, чем автоматически менять рабочее приложение.

Как уменьшить ложные срабатывания после первого сканирования?

Сначала удалите дубли и разделите стиль, дефекты и уязвимости. Уточняйте источники, приемники и проверенные санитайзеры, а исключение привязывайте к строке только с причиной и сроком пересмотра. Не отключайте целый набор из-за одного шумного правила.

Какой минимальный набор проверок подходит для CI?

Запускайте проверку типов, ESLint, Semgrep и аудит зависимостей по закрепленному lock-файлу. Для приложений с пользовательским HTML, секретами или административными функциями добавьте CodeQL. Версии анализаторов и наборов правил фиксируйте, чтобы сборка оставалась воспроизводимой.

Похожие статьи