O CodeQL 2.26.0 do GitHub adiciona uma consulta integrada que detecta padrões de prompt injection de IA, e a mudança já está fazendo com que os pipelines de CI sinalizem novos riscos. A atualização por si só não é suficiente — as equipes precisam de um fixture de regressão que garanta que a regra permaneça eficaz conforme o código evolui.
Por que um fixture de regressão é importante
O prompt injection permite que um invasor insira instruções maliciosas em um prompt que um modelo de linguagem seguirá posteriormente. Com a nova consulta, a análise estática pode rastrear dados de uma fonte não confiável até um sink do modelo. Se a regra for simplesmente ativada e nunca verificada, uma refatoração posterior pode quebrar o caminho do fluxo de dados e o alerta desaparecerá silenciosamente. Um fixture de regressão captura os caminhos exatos que devem disparar (ou não disparar) a regra, transformando o resultado da análise estática em um contrato que o build impõe.
Os três ingredientes de um fixture confiável
- Fonte não confiável – qualquer função que traga dados de fora da base de código confiável (ex: o corpo de uma issue do GitHub, um payload de webhook).
- Construção do prompt – o código que monta a requisição do modelo, tipicamente uma chamada a um SDK de cliente.
- Sink do modelo – o método do SDK que envia o prompt para o modelo. O mecanismo de fluxo de dados do CodeQL precisa ver uma chamada real da sua stack de produção para reconhecer o sink.
A consulta só é disparada quando todos os três estão presentes.
Organizando os arquivos de teste
Um layout convencional mantém a suíte fácil de auditar:
security-fixtures/prompt-injection/
├─ positive/
│ ├─ direct-flow.ts
│ └─ helper-flow.ts
├─ negative/
│ └─ trusted-instruction.ts
└─ expected-alerts.json
Arquivos positivos contêm código que deve ser sinalizado; arquivos negativos contêm padrões seguros que devem permanecer silenciosos.
Escrevendo os casos positivos
O exemplo mais simples mostra um fluxo direto de um valor não confiável para a chamada do modelo:
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,
});
}
Aqui, loadIssueBody é a fonte não confiável, model.generate é o sink, e os dados passam sem qualquer etapa de sanitização — exatamente o que a consulta foi projetada para capturar.
Um segundo caso positivo deve rotear os dados através de uma função auxiliar, provando que a análise segue caminhos indiretos:
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));
}
Ambos os arquivos pertencem a positive/.
Escrevendo o caso negativo
O fixture negativo deve demonstrar que a entrada do usuário não pode alterar a instrução do modelo. Um erro comum é assumir que uma função chamada sanitize() garante a segurança. O analisador estático não trata o nome como uma prova, portanto, o teste deve evitar qualquer stub de sanitização enganoso:
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",
});
}
Como os dados não confiáveis nunca chegam ao campo system, a regra deve permanecer silenciosa.
Declarando expectativas em JSON
O contrato da suíte reside em expected-alerts.json. Ele lista os alertas obrigatórios e os caminhos explicitamente proibidos:
{
"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"
]
}
Substitua USE_ACTUAL_RULE_ID pelo identificador mostrado na documentação do CodeQL ou na saída SARIF. Não tente adivinhar os IDs; a string exata é importante para a verificação do CI.
Integrando o fixture ao CI
- Fixe a versão do CodeQL CLI usada no pipeline na 2.26.0 (ou posterior).
- Construa um banco de dados descartável a partir do checkout atual antes de executar o fixture.
- Execute a consulta, capture os alertas e compare-os com o
expected-alerts.json. - Falhe o build se qualquer alerta obrigatório desaparecer ou se um caminho proibido começar a gerar alertas.
Não faça uma asserção do número total de alertas em todo o repositório — mudanças não relacionadas podem inflar o número e causar falhas falsas.
O que observar após uma atualização
Quando você atualizar o CodeQL para uma versão mais recente:
- O alerta obrigatório ainda aparece – prossiga com a revisão normal.
- O alerta obrigatório desaparece – bloqueie o build; investigue se a nova versão alterou a lógica da consulta ou se uma mudança no código quebrou o fluxo de dados.
- Um novo local positivo aparece – adicione-o à lista
requiredapós confirmar que é um caminho de injeção genuíno. - O controle negativo começa a gerar alertas – revise a estratégia de mitigação; a regra pode ter se tornado mais rigorosa.
A análise estática não pode provar como o modelo reagirá em tempo de execução. Complemente a suíte de regressão com testes adversariais que realmente enviem prompts elaborados para o modelo e verifiquem a resposta.
Conclusão
O CodeQL 2.26.0 oferece a capacidade de capturar bugs de prompt injection antes que eles sejam lançados, mas apenas se você consolidar essa capacidade com um fixture de regressão focado. Ao definir fontes não confiáveis, sinks de SDK reais e expectativas claras em um contrato JSON, você transforma uma regra de análise estática em um portão que impede regressões e força a atenção contínua em uma superfície de ataque que evolui rapidamente.
