CodeQL이 system prompt injection을 잡기 시작했다: AI 보안을 CI 데이터 흐름으로 바꾸는 법
CodeQL 2.26.0의 js/system-prompt-injection을 계기로 system prompt와 tool description의 신뢰 경계를 CI, 평가, 런타임 권한까지 연결하는 실무 체크리스트.
무슨 일이 있었나
AI 기능을 붙인 JavaScript·TypeScript 서비스에서 보안 검토는 대개 “프롬프트를 잘 써 두었는가”에 머물렀다. CodeQL 2.26.0은 이 문제를 더 익숙한 애플리케이션 보안 언어로 옮긴다. 사용자 제어 데이터라는 source가 system prompt 또는 tool description이라는 sink로 흐르는지를 정적 데이터 흐름 분석으로 찾는 것이다. 프롬프트 문장 자체보다 신뢰 경계가 코드에서 어떻게 연결됐는지를 묻기 시작했다는 점이 핵심이다.
GitHub는 2026년 7월 10일 CodeQL 2.26.0을 소개하며 JavaScript·TypeScript용 `js/system-prompt-injection` 쿼리를 추가했다고 밝혔다. 이 쿼리는 사용자 입력이 AI 모델의 system prompt로 흘러 모델 행동을 조작할 수 있는 경로를 탐지한다. 추가된 sink 모델에는 OpenAI, Anthropic, Google GenAI SDK의 여러 API가 포함되며 Sora prompt, OpenAI Realtime session instructions, Google GenAI cached content와 system instructions 같은 표면도 언급됐다.
쿼리 도움말은 범위를 system message에만 가두지 않는다. 에이전트에게 노출되는 tool description에 사용자 입력을 이어 붙이는 패턴도 같은 종류의 취약점으로 본다. 설명은 모델이 언제 어떤 도구를 선택할지 결정하는 신뢰된 지시문이기 때문이다. 권장 수정은 사용자 값을 user role 메시지나 구조화된 tool parameter로 옮기고, 정말 system prompt를 바꿔야 한다면 고정 allowlist로 제한하는 것이다.
왜 중요한가
이 변화가 중요한 이유는 코드 리뷰가 놓치기 쉬운 문자열 조합을 “source-to-sink 경로”로 보여 주기 때문이다. `instructions: base + req.query.persona`는 짧고 자연스러워 보인다. 그러나 persona가 사실상 임의 지시문 통로라면 출력 스타일 변경을 넘어 tool 선택, 데이터 조회, 후속 작업에 영향을 줄 수 있다. CodeQL 쿼리 도움말은 이 문제에 high precision, security severity 7.8을 부여하고 기본 JavaScript code-scanning suite에도 포함된다고 명시한다.
다만 이 탐지는 prompt injection 전체를 해결하지 않는다. 웹 문서, 검색 결과, 이메일, RAG chunk처럼 실행 중에 모델이 읽는 간접 입력은 코드에 고정된 데이터 흐름만으로 모두 증명하기 어렵다. 모델이 공격 문장을 따랐는지, 거부 품질이 정상 사용자를 해치지 않는지, tool 호출이 실제 권한을 넘었는지도 정적 분석만으로는 알 수 없다. 따라서 CodeQL은 “코드에서 만든 신뢰 경계 오류”를 잡는 첫 번째 층이지 완성된 방어막이 아니다.
커뮤니티가 말해 주는 빈틈
최근 DevSecOps 커뮤니티의 논의는 이 간극을 잘 보여 준다. 한 팀은 프로덕션 trace에서 발견한 실제 공격 문장을 PR마다 실행하는 평가 세트로 바꿨고, 댓글에서는 system prompt 유출, role 이탈, tool 오용, 고객 데이터 노출을 따로 평가하자는 의견이 나왔다. 이는 공식 기능의 증거가 아니라 현장 신호다. 정적 분석 경고 하나를 닫는 것과 모델·도구의 실제 행동을 반복 검증하는 것은 서로 다른 작업이라는 뜻이다.
개발·운영 영향
개발 프로세스에서는 AI 경로를 일반 입력 검증과 같은 수준으로 끌어올려야 한다. HTTP parameter, webhook payload, database field, fetched document가 `system`, `developer`, `instructions`, cached system content, tool description에 닿는지 inventory를 만든다. 경고가 나오면 문자열을 sanitize하는 데서 끝내지 말고, 그 값이 왜 더 높은 권한의 prompt에 들어가야 했는지 먼저 묻는다. 대부분은 user message나 enum parameter로 내려도 된다.
운영에서는 tool 권한이 마지막 안전선이어야 한다. 모델이 잘못된 지시를 따르더라도 읽기와 쓰기 도구를 분리하고, tenant scope를 서버에서 검증하고, destructive action에는 별도 승인과 idempotency key를 요구하면 피해 범위를 줄일 수 있다. system prompt 안에 secret을 넣지 않고, 모델 응답이 아니라 서버 측 policy가 접근 권한을 결정해야 한다. “프롬프트가 공격을 거부할 것”이라는 기대는 권한 제어가 아니다.
CI rollout은 세 단계가 현실적이다. 먼저 CodeQL 결과를 audit mode로 수집해 기존 부채와 false positive를 분류한다. 다음으로 새롭게 추가된 high-confidence 경로만 PR gate로 올리고, 동시에 악성·정상 경계 사례를 포함한 eval을 실행한다. 마지막으로 모델이나 SDK가 바뀔 때 adversarial corpus를 다시 돌리고, 실제 incident·trace에서 발견한 패턴을 append-only로 축적한다.
이번 주 적용 체크리스트
✓CodeQL JavaScript/TypeScript 분석이 최신 bundle을 사용하는지 확인한다.
✓`system`, `developer`, `instructions`, tool description으로 향하는 사용자 데이터 경로를 inventory한다.
✓자유 문자열을 user role 메시지나 구조화된 tool parameter로 내린다.
✓system prompt에 꼭 필요한 동적 값은 고정 allowlist와 enum으로 제한한다.
✓악성 입력뿐 아니라 정상인데 공격처럼 보이는 입력도 eval corpus에 넣는다.
✓tool 실행에는 tenant scope, 최소 권한, 승인, idempotency를 서버에서 적용한다.
✓모델·SDK·prompt 변경 시 정적 분석과 adversarial eval을 함께 재실행한다.
리스크와 반론
반론도 있다. 시스템 prompt에 사용자별 언어, 역할, 톤을 넣는 편이 구현이 쉽고 결과도 안정적일 수 있다. 그러나 자유 문자열 전체를 올릴 필요는 없다. `locale`, `persona`, `responseStyle`을 enum으로 모델링하고 서버가 검증한 값만 고정 템플릿에 매핑하면 제품 유연성과 신뢰 경계를 함께 지킬 수 있다. 반대로 모든 경고를 단순 억제하면 SDK가 sink 모델을 확장할 때 새 보호 효과도 잃는다.
이번 릴리스의 가장 큰 의미는 “프롬프트 보안”이 더 이상 별도의 마법 영역이 아니라는 데 있다. 입력 source, 신뢰된 instruction sink, path, allowlist, CI gate라는 기존 AppSec 도구를 그대로 적용할 수 있다. 다만 정적 분석을 만능으로 오해하지 말자. 코드 경로는 CodeQL로, 모델 행동은 eval과 red-team으로, 실제 피해 가능성은 최소 권한과 서버 측 정책으로 각각 줄여야 한다.