GitHub’s CodeQL 2.26.0 bổ sung một truy vấn tích hợp giúp phát hiện các mẫu tấn công chèn câu lệnh AI (AI prompt-injection), và sự thay đổi này đang khiến các đường ống CI (CI pipelines) bắt đầu cảnh báo các rủi ro mới. Chỉ nâng cấp thôi là chưa đủ—các nhóm cần một bộ kiểm thử hồi quy (regression test suite) để đảm bảo quy tắc này vẫn hoạt động hiệu quả khi mã nguồn tiến hóa.
Tại sao một fixture hồi quy lại quan trọng
Prompt injection cho phép kẻ tấn công lồng ghép các chỉ dẫn độc hại vào một câu lệnh mà mô hình ngôn ngữ sẽ thực hiện sau đó. Với truy vấn mới, phân tích tĩnh có thể truy vết dữ liệu từ một nguồn không đáng tin cậy đến một điểm cuối (sink) gọi mô hình. Nếu quy tắc chỉ đơn giản là được bật lên mà không bao giờ được xác minh, một lần tái cấu trúc (refactor) sau này có thể làm đứt gãy đường dẫn luồng dữ liệu và cảnh báo sẽ biến mất một cách âm thầm. Một fixture hồi quy sẽ ghi lại chính xác các đường dẫn cần phải kích hoạt (hoặc không kích hoạt) quy tắc, biến kết quả phân tích tĩnh thành một "hợp đồng" mà quá trình build sẽ thực thi.
Ba thành phần của một fixture đáng tin cậy
- Nguồn không đáng tin cậy (Untrusted source) – bất kỳ hàm nào đưa dữ liệu từ bên ngoài cơ sở mã nguồn tin cậy vào (ví dụ: nội dung một GitHub issue, payload của một webhook).
- Xây dựng câu lệnh (Prompt construction) – mã nguồn lắp ráp yêu cầu gửi tới mô hình, thường là một lời gọi đến client SDK.
- Điểm cuối mô hình (Model sink) – phương thức SDK gửi câu lệnh tới mô hình. Công cụ luồng dữ liệu (data-flow engine) của CodeQL cần thấy một lời gọi thực tế từ stack production của bạn để nhận diện được sink.
Chỉ khi có mặt cả ba thành phần này thì truy vấn mới được kích hoạt.
Tổ chức các tệp kiểm thử
Một cách bố trí truyền thống giúp bộ kiểm thử dễ dàng được kiểm chứng:
security-fixtures/prompt-injection/
├─ positive/
│ ├─ direct-flow.ts
│ └─ helper-flow.ts
├─ negative/
│ └─ trusted-instruction.ts
└─ expected-alerts.json
Các tệp positive chứa mã nguồn đáng lẽ phải bị gắn cờ; các tệp negative chứa các mẫu an toàn mà không được phép phát cảnh báo.
Viết các trường hợp dương tính (positive cases)
Ví dụ đơn giản nhất cho thấy luồng dữ liệu trực tiếp từ một giá trị không đáng tin cậy đến lời gọi mô hình:
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,
});
}
Ở đây loadIssueBody là nguồn không đáng tin cậy, model.generate là sink, và dữ liệu đi qua mà không có bất kỳ bước làm sạch (sanitisation) nào—đúng chính xác là những gì truy vấn được thiết kế để bắt được.
Trường hợp dương tính thứ hai nên điều hướng dữ liệu qua một hàm bổ trợ (helper function), nhằm chứng minh rằng việc phân tích có thể theo dõi các đường dẫn gián tiếp:
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));
}
Cả hai tệp này đều thuộc thư mục positive/.
Viết trường hợp âm tính (negative case)
Fixture âm tính phải chứng minh được rằng đầu vào của người dùng không thể làm thay đổi chỉ dẫn của mô hình. Một sai lầm phổ biến là giả định rằng một hàm có tên sanitize() sẽ đảm bảo an toàn. Trình phân tích tĩnh không coi cái tên đó là một bằng chứng, vì vậy bài kiểm tra nên tránh bất kỳ stub làm sạch dữ liệu gây hiểu lầm nào:
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",
});
}
Vì dữ liệu không đáng tin cậy không bao giờ chạm tới trường system, quy tắc sẽ giữ im lặng.
Khai báo kỳ vọng trong JSON
"Hợp đồng" của bộ kiểm thử nằm trong expected-alerts.json. Nó liệt kê các cảnh báo bắt buộc và các đường dẫn bị cấm một cách rõ ràng:
{
"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"
]
}
Thay thế USE_ACTUAL_RULE_ID bằng mã định danh được hiển thị trong tài liệu CodeQL hoặc đầu ra SARIF. Đừng đoán ID; chuỗi ký tự chính xác rất quan trọng đối với việc kiểm tra CI.
Kết nối fixture vào CI
- Cố định (Pin) phiên bản CodeQL CLI được sử dụng trong pipeline ở bản 2.26.0 (hoặc mới hơn).
- Xây dựng một cơ sở dữ liệu tạm thời (disposable database) từ bản checkout hiện tại trước khi chạy fixture.
- Chạy truy vấn, thu thập các cảnh báo và so sánh chúng với
expected-alerts.json. - Làm thất bại quá trình build nếu bất kỳ cảnh báo bắt buộc nào biến mất hoặc nếu một đường dẫn bị cấm bắt đầu phát cảnh báo.
Đừng khẳng định (assert) tổng số lượng cảnh báo trên toàn bộ repository—những thay đổi không liên quan có thể làm tăng số lượng và gây ra các lỗi sai (false failures).
Những điều cần lưu ý sau khi nâng cấp
Khi bạn nâng cấp CodeQL lên phiên bản mới hơn:
- Cảnh báo bắt buộc vẫn xuất hiện – tiếp tục quy trình xem xét bình thường.
- Cảnh báo bắt buộc biến mất – chặn quá trình build; điều tra xem liệu phiên bản mới có thay đổi logic truy vấn hay liệu một thay đổi mã nguồn đã làm đứt gãy luồng dữ liệu hay không.
- Vị trí dương tính mới xuất hiện – thêm nó vào danh sách
requiredsau khi xác nhận đó là một đường dẫn chèn câu lệnh thực sự. - Kiểm soát âm tính bắt đầu phát cảnh báo – xem xét lại chiến lược giảm thiểu; quy tắc có thể đã trở nên nghiêm ngặt hơn.
Phân tích tĩnh không thể chứng minh mô hình sẽ phản ứng như thế nào khi chạy (runtime). Hãy bổ sung bộ kiểm thử hồi quy bằng các bài kiểm thử đối kháng (adversarial tests) thực sự gửi các câu lệnh được soạn thảo kỹ lưỡng tới mô hình và xác minh phản hồi.
Bài học rút ra
CodeQL 2.26.0 mang lại cho bạn khả năng bắt được các lỗi prompt-injection trước khi chúng được phát hành, nhưng chỉ khi bạn khóa chặt khả năng đó bằng một fixture hồi quy tập trung. Bằng cách xác định các nguồn không đáng tin cậy, các sink SDK thực tế và các kỳ vọng rõ ràng trong một hợp đồng JSON, bạn biến một quy tắc phân tích tĩnh thành một rào chắn ngăn chặn các lỗi hồi quy và buộc phải chú ý liên tục vào một bề mặt tấn công đang tiến hóa nhanh chóng.
