CodeQL 2.26.0 от GitHub добавляет встроенный запрос, который обнаруживает паттерны prompt-injection (инъекции промптов) для ИИ, и это изменение уже заставляет CI-конвейеры помечать новые риски. Одного обновления недостаточно — командам нужен набор регрессионных тестов, который гарантирует эффективность правила по мере развития кода.

Почему важна регрессионная фикстура

Prompt injection позволяет злоумышленнику внедрить вредоносные инструкции в промпт, которым языковая модель впоследствии будет следовать. С помощью нового запроса статический анализ может проследить путь данных от ненадежного источника до «приемника» (sink), вызывающего модель. Если просто включить правило и никогда его не проверять, последующий рефакторинг может нарушить путь передачи данных, и оповещение бесследно исчезнет. Регрессионная фикстура фиксирует именно те пути, которые должны вызывать (или не вызывать) срабатывание правила, превращая результат статического анализа в контракт, который контролируется при сборке.

Три составляющих надежной фикстуры

  1. Ненадежный источник (Untrusted source) — любая функция, которая получает данные извне доверенной кодовой базы (например, тело GitHub issue, полезная нагрузка webhook).
  2. Конструирование промпта (Prompt construction) — код, который формирует запрос к модели, обычно это вызов клиентского SDK.
  3. Приемник модели (Model sink) — метод SDK, который отправляет промпт модели. Движку анализа потоков данных (data-flow engine) CodeQL необходимо увидеть реальный вызов из вашего продакшн-стека, чтобы распознать приемник.

Запрос срабатывает только при наличии всех трех компонентов.

Организация тестовых файлов

Традиционная структура упрощает аудит набора тестов:

security-fixtures/prompt-injection/
├─ positive/
│  ├─ direct-flow.ts
│  └─ helper-flow.ts
├─ negative/
│  └─ trusted-instruction.ts
└─ expected-alerts.json

Положительные (positive) файлы содержат код, который должен быть помечен как подозрительный; отрицательные (negative) файлы содержат безопасные паттерны, которые не должны вызывать срабатываний.

Написание положительных сценариев

Самый простой пример демонстрирует прямой поток от ненадежного значения к вызову модели:

import { model } from "./supported-client";

declare function loadIssueBody(id: number): Promise<string>;

export async function summarize(id: number) {
  const untrusted = await loadIssueBody(id);
  return model.generate({
    system: "Summarize the issue",
    user: untrusted,
  });
}

Здесь loadIssueBody является ненадежным источником, model.generate — приемником, а данные передаются без какого-либо этапа очистки (sanitisation) — именно то, что призван обнаружить этот запрос.

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

function wrapUserInput(input: string) {
  return { system: "Summarize the issue", user: input };
}

export async function summarizeViaHelper(id: number) {
  const raw = await loadIssueBody(id);
  return model.generate(wrapUserInput(raw));
}

Оба файла должны находиться в директории positive/.

Написание отрицательного сценария

Отрицательная фикстура должна демонстрировать, что пользовательский ввод не может изменить инструкцию модели. Распространенная ошибка — полагать, что функция с названием sanitize() гарантирует безопасность. Статический анализатор не воспринимает имя функции как доказательство, поэтому в тесте следует избегать любых вводящих в заблуждение заглушек очистки:

export async function safeSummarize(id: number) {
  const trusted = "Summarize the issue";
  const user = await loadIssueBody(id); // not used in the system prompt
  return model.generate({
    system: trusted,
    user: "Static placeholder",
  });
}

Поскольку ненадежные данные никогда не достигают поля system, правило не должно срабатывать.

Объявление ожиданий в JSON

Контракт набора тестов хранится в expected-alerts.json. В нем перечислены обязательные оповещения и явно запрещенные пути:

{
  "required": [
    {
      "ruleId": "USE_ACTUAL_RULE_ID",
      "pathSuffix": "positive/direct-flow.ts"
    },
    {
      "ruleId": "USE_ACTUAL_RULE_ID",
      "pathSuffix": "positive/helper-flow.ts"
    }
  ],
  "forbiddenPathSuffixes": [
    "negative/trusted-instruction.ts"
  ]
}

Замените USE_ACTUAL_RULE_ID на идентификатор, указанный в документации CodeQL или в выводе SARIF. Не пытайтесь угадать ID; для проверки в CI важно точное соответствие строки.

Интеграция фикстуры в CI

  1. Зафиксируйте версию CodeQL CLI в конвейере на уровне 2.26.0 (или выше).
  2. Перед запуском фикстуры создайте временную базу данных из текущего состояния репозитория (checkout).
  3. Запустите запрос, соберите оповещения и сравните их с expected-alerts.json.
  4. Прервите сборку, если какое-либо обязательное оповещение исчезло или если начал срабатывать запрещенный путь.

Не проверяйте общее количество оповещений по всему репозиторию — несвязанные изменения могут увеличить это число и привести к ложным сбоям.

На что обратить внимание после обновления

Когда вы обновляете CodeQL до новой версии:

  • Обязательное оповещение по-прежнему появляется — продолжайте обычную проверку.
  • Обязательное оповещение исчезло — заблокируйте сборку; выясните, изменила ли новая версия логику запроса или же изменения в коде нарушили поток данных.
  • Появилось новое положительное место срабатывания — добавьте его в список required после подтверждения того, что это реальный путь инъекции.
  • Отрицательный контрольный тест начал срабатывать — пересмотрите стратегию защиты; возможно, правило стало более строгим.

Статический анализ не может доказать, как модель поведет себя во время выполнения. Дополните регрессионный набор тестов состязательными (adversarial) тестами, которые фактически отправляют специально сформированные промпты модели и проверяют ответ.

Итог

CodeQL 2.26.0 дает возможность обнаруживать баги prompt-injection до того, как они попадут в продакшн, но только если вы закрепите эту возможность с помощью специализированной регрессионной фикстуры. Определяя ненадежные источники, реальные приемники SDK и четкие ожидания в JSON-контракте, вы превращаете правило статического анализа в барьер, который предотвращает регрессии и заставляет постоянно следить за быстро меняющейся поверхностью атаки.