Содержание
Коротко
SQL-инъекции в Node.js не исчезли: TypeScript и ревью кода часто пропускают три привычных шаблона в node-postgres. На Dev.to показали, как правило eslint-plugin-pg ловит их статически — прямо в редакторе, без тяжёлого статического анализа безопасности.
Что произошло
Сценарий знакомый: линтер чист, ревьюер одобрил, релиз ушёл в прод. Через три месяца пентестер находит строку вроде pool.query("SELECT * FROM orders WHERE user_id = " + req.query.userId). Параметризованные запросы все знают, OWASP давно помечает инъекции как критичный риск — но уязвимости всё равно проскакивают, потому что каждый шаблон выглядит безобидно по одной строке.
Автор выделяет три формы. Прямая конкатенация в первом аргументе .query(). Шаблонные строки с ${userId} — синтаксис чище, риск тот же. Присваивание в переменную: const sql = "…" + category и отдельно client.query(sql) — на вызове ничего не бросается в глаза, а отравление строки случилось двумя строками выше.
Правило pg/no-unsafe-query заточено под pg, а не под «любую строку рядом с SELECT». Оно срабатывает только на .query(), понимает контракт $1 + массив значений и помечает переменную «загрязнённой» в момент присваивания. Обёртка sanitize() не спасает — правило не может доказать, что она эквивалентна параметризации.
Почему это важно
Универсальные линтеры либо шумят на безобидную сборку строк, либо пропускают шаблонные литералы. ESLint в пределах одной функции не отслеживает «загрязнение» через весь проект, зато работает на каждом сохранении и в хуке перед коммитом — там, где разработчик реально правит SQL.
Команды на Prisma, Knex или Drizzle тоже не застрахованы: $queryRaw, knex.raw и аналоги возвращают те же три шаблона. Для сервисов на чистом pg — внутренние API, пайплайны, микросервисы — поверхность атаки как раз здесь.
На практике
- Подключите
eslint-plugin-pgи включитеpg/no-unsafe-queryкак ошибку сборки. - Везде, где пользовательский ввод попадает в SQL, используйте
$1,$2и второй аргумент-массив — не конкатенацию и не${}без плейсхолдеров$N. - Не «рефакторите» параметризованный запрос в «читаемую» строку — типичный путь к регрессии.
- Для имён схем/таблиц —
pg-formatили отдельный слой, а не+ SCHEMA_NAME(известный ложноположительный случай правила). - Semgrep/CodeQL оставьте для межпроцедурного анализа; ESLint — для мгновенной обратной связи в редакторе.
Итог
SQL-инъекции в Node.js — не проблема «невежественных джунов», а проблема шаблонов, которые обманывают ревью. Специализированное ESLint-правило для pg закрывает три реальных формы; материал на Dev.to стоит прочитать, если у вас в коде ещё живёт query("…" + id).