CodeQL 2.26.0 от GitHub добавляет встроенный запрос, который обнаруживает паттерны prompt-injection (инъекции промптов) для ИИ, и это изменение уже заставляет CI-конвейеры помечать новые риски. Одного обновления недостаточно — командам нужен набор регрессионных тестов, который гарантирует эффективность правила по мере развития кода.
Почему важна регрессионная фикстура
Prompt injection позволяет злоумышленнику внедрить вредоносные инструкции в промпт, которым языковая модель впоследствии будет следовать. С помощью нового запроса статический анализ может проследить путь данных от ненадежного источника до «приемника» (sink), вызывающего модель. Если просто включить правило и никогда его не проверять, последующий рефакторинг может нарушить путь передачи данных, и оповещение бесследно исчезнет. Регрессионная фикстура фиксирует именно те пути, которые должны вызывать (или не вызывать) срабатывание правила, превращая результат статического анализа в контракт, который контролируется при сборке.
Три составляющих надежной фикстуры
- Ненадежный источник (Untrusted source) — любая функция, которая получает данные извне доверенной кодовой базы (например, тело GitHub issue, полезная нагрузка webhook).
- Конструирование промпта (Prompt construction) — код, который формирует запрос к модели, обычно это вызов клиентского SDK.
- Приемник модели (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
- Зафиксируйте версию CodeQL CLI в конвейере на уровне 2.26.0 (или выше).
- Перед запуском фикстуры создайте временную базу данных из текущего состояния репозитория (checkout).
- Запустите запрос, соберите оповещения и сравните их с
expected-alerts.json. - Прервите сборку, если какое-либо обязательное оповещение исчезло или если начал срабатывать запрещенный путь.
Не проверяйте общее количество оповещений по всему репозиторию — несвязанные изменения могут увеличить это число и привести к ложным сбоям.
На что обратить внимание после обновления
Когда вы обновляете CodeQL до новой версии:
- Обязательное оповещение по-прежнему появляется — продолжайте обычную проверку.
- Обязательное оповещение исчезло — заблокируйте сборку; выясните, изменила ли новая версия логику запроса или же изменения в коде нарушили поток данных.
- Появилось новое положительное место срабатывания — добавьте его в список
requiredпосле подтверждения того, что это реальный путь инъекции. - Отрицательный контрольный тест начал срабатывать — пересмотрите стратегию защиты; возможно, правило стало более строгим.
Статический анализ не может доказать, как модель поведет себя во время выполнения. Дополните регрессионный набор тестов состязательными (adversarial) тестами, которые фактически отправляют специально сформированные промпты модели и проверяют ответ.
Итог
CodeQL 2.26.0 дает возможность обнаруживать баги prompt-injection до того, как они попадут в продакшн, но только если вы закрепите эту возможность с помощью специализированной регрессионной фикстуры. Определяя ненадежные источники, реальные приемники SDK и четкие ожидания в JSON-контракте, вы превращаете правило статического анализа в барьер, который предотвращает регрессии и заставляет постоянно следить за быстро меняющейся поверхностью атаки.
