CodeQL 2.26.0 ของ GitHub เพิ่ม built-in query ที่ตรวจจับรูปแบบ prompt injection ของ AI และการเปลี่ยนแปลงนี้กำลังทำให้ CI pipelines เริ่มแจ้งเตือนความเสี่ยงใหม่ๆ การอัปเกรดเพียงอย่างเดียวไม่เพียงพอ—ทีมจำเป็นต้องมีชุดทดสอบ regression ที่รับประกันได้ว่ากฎนี้จะยังคงมีประสิทธิภาพเมื่อโค้ดมีการพัฒนาต่อไป
ทำไม regression fixture ถึงสำคัญ
Prompt injection ช่วยให้ผู้โจมตีสามารถแทรกคำสั่งที่เป็นอันตรายเข้าไปใน prompt ซึ่งโมเดลภาษาจะทำตามในภายหลัง ด้วย query ใหม่นี้ การวิเคราะห์แบบ static สามารถติดตามข้อมูลจากแหล่งที่ไม่น่าเชื่อถือ (untrusted source) ไปจนถึง sink ที่เรียกใช้งานโมเดล (model-calling sink) ได้ หากเพียงแค่เปิดใช้งานกฎนี้โดยไม่มีการตรวจสอบ หากมีการ refactor โค้ดในภายหลัง อาจทำให้เส้นทางการไหลของข้อมูล (data-flow path) ขาดตอน และการแจ้งเตือนจะหายไปอย่างเงียบๆ regression fixture จะช่วยบันทึกเส้นทางที่ถูกต้องที่ควรจะกระตุ้น (หรือไม่กระตุ้น) กฎนี้ เปลี่ยนผลลัพธ์จากการวิเคราะห์แบบ static ให้กลายเป็นข้อตกลง (contract) ที่การ build จะต้องปฏิบัติตาม
3 องค์ประกอบของ fixture ที่เชื่อถือได้
- Untrusted source – ฟังก์ชันใดก็ตามที่นำข้อมูลมาจากภายนอกฐานโค้ดที่เชื่อถือได้ (เช่น เนื้อหาใน GitHub issue, webhook payload)
- Prompt construction – โค้ดที่ประกอบคำขอโมเดล ซึ่งโดยปกติจะเป็นการเรียกใช้ client SDK
- Model sink – เมธอดของ SDK ที่ส่ง prompt ไปยังโมเดล เครื่องมือ data-flow engine ของ CodeQL จำเป็นต้องเห็นการเรียกใช้งานจริงจาก production stack ของคุณเพื่อที่จะระบุ sink ได้
query จะทำงานก็ต่อเมื่อมีครบทั้งสามองค์ประกอบนี้เท่านั้น
การจัดระเบียบไฟล์ทดสอบ
การวางโครงสร้างแบบมาตรฐานจะช่วยให้ชุดทดสอบตรวจสอบได้ง่าย:
security-fixtures/prompt-injection/
├─ positive/
│ ├─ direct-flow.ts
│ └─ helper-flow.ts
├─ negative/
│ └─ trusted-instruction.ts
└─ expected-alerts.json
ไฟล์ Positive จะบรรจุโค้ดที่ควรถูกตรวจพบ (flagged); ส่วนไฟล์ negative จะบรรจุรูปแบบที่ปลอดภัยซึ่งต้องไม่ถูกตรวจพบ (stay silent)
การเขียน positive cases
ตัวอย่างที่ง่ายที่สุดแสดงการไหลของข้อมูลโดยตรงจากค่าที่ไม่น่าเชื่อถือไปยังการเรียกโมเดล:
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 คือ untrusted source, model.generate คือ sink และข้อมูลถูกส่งผ่านโดยไม่มีขั้นตอนการทำ sanitisation เลย—ซึ่งเป็นสิ่งที่ query ถูกออกแบบมาเพื่อตรวจจับโดยเฉพาะ
positive case ที่สองควรส่งข้อมูลผ่าน helper function เพื่อพิสูจน์ว่าการวิเคราะห์สามารถติดตามเส้นทางทางอ้อมได้:
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 case
negative fixture ต้องแสดงให้เห็นว่า input ของผู้ใช้ไม่สามารถเปลี่ยนแปลงคำสั่งของโมเดลได้ ข้อผิดพลาดที่พบบ่อยคือการทึกทักเอาเองว่าฟังก์ชันที่ชื่อ sanitize() จะรับประกันความปลอดภัยได้ ตัววิเคราะห์แบบ static ไม่ได้มองว่าชื่อฟังก์ชันคือข้อพิสูจน์ ดังนั้นการทดสอบควรหลีกเลี่ยงการใช้ stub สำหรับการทำ sanitisation ที่อาจทำให้เข้าใจผิด:
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 ซึ่งจะระบุ alert ที่จำเป็นต้องมี และเส้นทางที่ต้องห้ามอย่างชัดเจน:
{
"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 ด้วย identifier ที่แสดงในเอกสารของ CodeQL หรือผลลัพธ์จาก SARIF อย่าเดา ID เอง เพราะสตริงที่ถูกต้องมีความสำคัญต่อการตรวจสอบใน CI
การเชื่อมต่อ fixture เข้ากับ CI
- ล็อกเวอร์ชันของ CodeQL CLI ที่ใช้ใน pipeline ให้เป็น 2.26.0 (หรือใหม่กว่า)
- สร้างฐานข้อมูลแบบใช้แล้วทิ้ง (disposable database) จากการ checkout ปัจจุบันก่อนรัน fixture
- รัน query, เก็บ alert และนำไปเปรียบเทียบกับ
expected-alerts.json - ทำให้การ build ล้มเหลวหาก alert ที่จำเป็นหายไป หรือหากมีเส้นทางที่ต้องห้ามเริ่มมีการแจ้งเตือน
อย่าใช้การตรวจสอบจำนวน alert ทั้งหมดใน repository—การเปลี่ยนแปลงที่ไม่เกี่ยวข้องอาจทำให้จำนวนเพิ่มขึ้นและทำให้เกิด false failures ได้
สิ่งที่ต้องเฝ้าระวังหลังการอัปเกรด
เมื่อคุณอัปเกรด CodeQL เป็นเวอร์ชันที่ใหม่กว่า:
- Required alert ยังคงปรากฏอยู่ – ดำเนินการตรวจสอบตามปกติ
- Required alert หายไป – สั่งระงับการ build; ตรวจสอบว่าเวอร์ชันใหม่มีการเปลี่ยน logic ของ query หรือการเปลี่ยนแปลงโค้ดทำให้ data flow ขาดตอนหรือไม่
- พบตำแหน่ง positive ใหม่ – เพิ่มเข้าไปในรายการ
requiredหลังจากยืนยันแล้วว่าเป็นเส้นทางการฉีดคำสั่ง (injection path) จริงๆ - Negative control เริ่มมีการแจ้งเตือน – ทบทวนกลยุทธ์การป้องกัน (mitigation strategy) อีกครั้ง กฎอาจจะมีความเข้มงวดมากขึ้น
การวิเคราะห์แบบ static ไม่สามารถพิสูจน์ได้ว่าโมเดลจะมีปฏิกิริยาอย่างไรในขณะ runtime ควรใช้ regression suite ควบคู่ไปกับ adversarial tests ที่ส่ง prompt ที่สร้างขึ้นมาไปยังโมเดลจริงๆ เพื่อตรวจสอบการตอบสนอง
บทสรุป
CodeQL 2.26.0 ช่วยให้คุณสามารถตรวจจับบั๊ก prompt injection ได้ก่อนที่จะถูกปล่อยออกไป แต่ต้องทำได้ก็ต่อเมื่อคุณล็อกความสามารถนั้นไว้ด้วย regression fixture ที่เจาะจง การกำหนด untrusted sources, SDK sinks ที่ใช้งานจริง และความคาดหวังที่ชัดเจนใน JSON contract จะเปลี่ยนกฎการวิเคราะห์แบบ static ให้กลายเป็นด่านตรวจที่หยุดยั้ง regression และบังคับให้ต้องเฝ้าระวัง attack surface ที่เปลี่ยนแปลงอย่างรวดเร็วอยู่เสมอ
