GitHub의 CodeQL 2.26.0은 AI 프롬프트 인젝션 패턴을 탐지하는 내장 쿼리를 추가했으며, 이 변화로 인해 CI 파이프라인에서 새로운 리스크가 이미 감지되고 있습니다. 업그레이드만으로는 충분하지 않습니다. 팀은 코드가 진화함에 따라 해당 규칙이 효과적으로 유지되도록 보장하는 회귀 테스트 스위트(regression test suite)가 필요합니다.
회귀 픽스처(regression fixture)가 중요한 이유
프롬프트 인젝션은 공격자가 언어 모델이 나중에 따르게 될 악성 지침을 프롬프트에 몰래 끼워 넣는 방식입니다. 새로운 쿼리를 사용하면 정적 분석을 통해 신뢰할 수 없는 소스(untrusted source)에서 모델 호출 싱크(model-calling sink)까지 데이터를 추적할 수 있습니다. 만약 규칙을 단순히 켜두기만 하고 검증하지 않는다면, 나중에 진행된 리팩토링으로 인해 데이터 흐름 경로가 끊겨 경고가 조용히 사라질 수 있습니다. 회귀 픽스처는 규칙을 트리거해야 하는(또는 트리거하지 않아야 하는) 정확한 경로를 캡처하여, 정적 분석 결과를 빌드에서 강제할 수 있는 계약(contract)으로 변환합니다.
신뢰할 수 있는 픽스처를 위한 세 가지 요소
- Untrusted source – 신뢰할 수 있는 코드 베이스 외부에서 데이터를 가져오는 모든 함수 (예: GitHub 이슈 본문, 웹훅 페이로드).
- Prompt construction – 모델 요청을 구성하는 코드, 일반적으로 클라이언트 SDK 호출.
- Model sink – 프롬프트를 모델로 전송하는 SDK 메서드. CodeQL의 데이터 흐름 엔진이 싱크를 인식하려면 프로덕션 스택의 실제 호출을 확인해야 합니다.
이 세 가지가 모두 존재할 때만 쿼리가 실행됩니다.
테스트 파일 구성하기
전형적인 레이아웃은 스위트를 감사하기 쉽게 유지합니다:
security-fixtures/prompt-injection/
├─ positive/
│ ├─ direct-flow.ts
│ └─ helper-flow.ts
├─ negative/
│ └─ trusted-instruction.ts
└─ expected-alerts.json
Positive 파일은 플래그가 지정되어야 하는 코드를 포함하며, negative 파일은 경고가 발생하지 않아야 하는 안전한 패턴을 담고 있습니다.
Positive 케이스 작성하기
가장 간단한 예시는 신뢰할 수 없는 값에서 모델 호출로 이어지는 직접적인 흐름을 보여줍니다:
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) 단계 없이 전달됩니다. 이는 쿼리가 포착하도록 설계된 바로 그 상황입니다.
두 번째 positive 케이스는 데이터를 헬퍼 함수를 통해 라우팅하여, 분석이 간접적인 경로를 따르는지 증명해야 합니다:
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/ 디렉토리에 속합니다.
Negative 케이스 작성하기
Negative 픽스처는 사용자 입력이 모델의 지침을 변경할 수 없음을 증명해야 합니다. 흔한 실수는 sanitize()라고 불리는 함수가 안전을 보장한다고 가정하는 것입니다. 정적 분석기는 함수 이름을 증거로 취급하지 않으므로, 테스트 시 오해를 불러일으킬 수 있는 정제 스텁(sanitisation stub)을 피해야 합니다:
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(또는 그 이후 버전)으로 고정합니다.
- 픽스처를 실행하기 전에 현재 체크아웃된 코드로부터 일회성 데이터베이스를 빌드합니다.
- 쿼리를 실행하고, 경고를 캡처한 다음, 이를
expected-alerts.json과 비교합니다. - 필수 경고가 사라지거나 금지된 경로에서 경고가 발생하기 시작하면 빌드를 실패 처리합니다.
저장소 전체의 총 경고 수를 확인하지 마십시오. 관련 없는 변경 사항으로 인해 경고 수가 늘어나 잘못된 실패(false failure)가 발생할 수 있습니다.
업그레이드 후 주의 깊게 살펴볼 사항
CodeQL을 최신 버전으로 올릴 때:
- 필수 경고가 여전히 나타남 – 정상적인 검토를 진행합니다.
- 필수 경고가 사라짐 – 빌드를 차단합니다. 새 버전에서 쿼리 로직이 변경되었는지, 아니면 코드 변경으로 인해 데이터 흐름이 깨졌는지 조사합니다.
- 새로운 positive 위치가 나타남 – 실제 인젝션 경로인지 확인한 후
required목록에 추가합니다. - Negative 제어 항목에서 경고가 발생함 – 완화 전략을 재검토합니다. 규칙이 더 엄격해졌을 수 있습니다.
정적 분석은 런타임에 모델이 어떻게 반응할지 증명할 수 없습니다. 정교하게 제작된 프롬프트를 실제로 모델에 보내고 응답을 확인하는 적대적 테스트(adversarial tests)로 회귀 스위트를 보완하십시오.
요약
CodeQL 2.26.0은 프롬프트 인젝션 버그가 배포되기 전에 포착할 수 있는 능력을 제공하지만, 이는 집중적인 회귀 픽스처로 그 능력을 고정할 때만 유효합니다. 신뢰할 수 없는 소스, 실제 SDK 싱크, 그리고 JSON 계약을 통한 명확한 기대 결과를 정의함으로써, 정적 분석 규칙을 회귀를 방지하고 빠르게 진화하는 공격 표면에 대해 지속적인 주의를 강제하는 게이트(gate)로 전환할 수 있습니다.
