نسخه 2.26.0 CodeQL در GitHub یک پرس‌وجوی (query) داخلی اضافه کرده است که الگوهای تزریق دستور (prompt-injection) در هوش مصنوعی را شناسایی می‌کند، و این تغییر از همین حالا باعث شده تا خط لوله‌های CI ریسک‌های جدیدی را گزارش کنند. ارتقا به تنهایی کافی نیست؛ تیم‌ها به یک مجموعه تست رگرسیون (regression test suite) نیاز دارند که تضمین کند با تکامل کد، این قانون همچنان مؤثر باقی می‌ماند.

چرا یک فیکسچر رگرسیون اهمیت دارد

تزریق دستور (Prompt injection) به مهاجم اجازه می‌دهد دستورات مخربی را در یک پرامپت بگنجاند که مدل زبانی بعداً آن‌ها را اجرا می‌کند. با پرس‌وجوی جدید، تحلیل استاتیک می‌تواند داده‌ها را از یک منبع غیرقابل اعتماد (untrusted source) تا یک مقصد (sink) که مدل را فراخوانی می‌کند، ردیابی کند. اگر قانون صرفاً فعال شود و هرگز تأیید نشود، یک بازنویسی (refactor) در آینده می‌تواند مسیر جریان داده را قطع کند و هشدار به‌طور بی‌صدا ناپدید شود. یک فیکسچر رگرسیون، مسیرهای دقیقی را که باید باعث فعال شدن (یا نشدن) قانون شوند، ثبت می‌کند و نتیجه تحلیل استاتیک را به قراردادی تبدیل می‌کند که فرآیند build آن را اعمال می‌کند.

سه رکن اصلی یک فیکسچر قابل اعتماد

  1. منبع غیرقابل اعتماد (Untrusted source) – هر تابعی که داده‌ها را از خارج از پایگاه کد مورد اعتماد وارد می‌کند (مثلاً بدنه یک GitHub issue یا یک webhook payload).
  2. ساخت پرامپت (Prompt construction) – کدی که درخواست مدل را سرهم می‌کند، که معمولاً یک فراخوانی به یک SDK کلاینت است.
  3. مقصد مدل (Model sink) – متد SDK که پرامپت را به مدل ارسال می‌کند. موتور جریان داده (data-flow engine) در CodeQL باید یک فراخوانی واقعی از استک تولید (production) شما را ببیند تا مقصد را شناسایی کند.

تنها زمانی که هر سه مورد حضور داشته باشند، پرس‌وجو اجرا می‌شود.

سازماندهی فایل‌های تست

یک ساختار متداول باعث می‌شود بازرسی مجموعه تست آسان باشد:

security-fixtures/prompt-injection/
├─ positive/
│  ├─ direct-flow.ts
│  └─ helper-flow.ts
├─ negative/
│  └─ trusted-instruction.ts
└─ expected-alerts.json

فایل‌های مثبت (positive) حاوی کدی هستند که باید شناسایی شوند؛ فایل‌های منفی (negative) الگوهای ایمنی را نگه می‌دارند که باید بدون هشدار باقی بمانند.

نوشتن موارد مثبت

ساده‌ترین مثال، یک جریان مستقیم از یک مقدار غیرقابل اعتماد به فراخوانی مدل را نشان می‌دهد:

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 مقصد (sink) است و داده‌ها بدون هیچ مرحله پاک‌سازی (sanitisation) عبور می‌کنند؛ دقیقاً همان چیزی که این پرس‌وجو برای شناسایی طراحی شده است.

دومین مورد مثبت باید داده‌ها را از طریق یک تابع کمکی عبور دهد تا ثابت کند تحلیل، مسیرهای غیرمستقیم را نیز دنبال می‌کند:

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/ قرار بگیرند.

نوشتن مورد منفی

فیکسچر منفی باید نشان دهد که ورودی کاربر نمی‌تواند دستور مدل را تغییر دهد. یک اشتباه رایج این است که فرض شود تابعی به نام sanitize() ایمنی را تضمین می‌کند. تحلیل‌گر استاتیک، نام تابع را به عنوان مدرک در نظر نمی‌گیرد، بنابراین تست باید از هرگونه استاب (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 نشان داده شده استفاده کنید. شناسه‌ها را حدس نزنید؛ رشته دقیق برای بررسی CI اهمیت دارد.

متصل کردن فیکسچر به CI

  1. نسخه CodeQL CLI مورد استفاده در خط لوله (pipeline) را روی 2.26.0 (یا بالاتر) ثابت کنید.
  2. قبل از اجرای فیکسچر، یک پایگاه داده موقت از آخرین نسخه کد (checkout) بسازید.
  3. پرس‌وجو را اجرا کنید، هشدارها را ثبت کنید و آن‌ها را با expected-alerts.json مقایسه کنید.
  4. اگر هر یک از هشدارهای مورد نیاز ناپدید شد یا اگر یک مسیر ممنوعه شروع به هشدار دادن کرد، عملیات build را با شکست مواجه کنید.

تعداد کل هشدارها را در کل مخزن (repository) بررسی نکنید؛ تغییرات بی‌ربط می‌توانند تعداد را افزایش داده و باعث شکست‌های کاذب شوند.

مواردی که باید پس از ارتقا زیر نظر داشت

هنگامی که CodeQL را به نسخه جدیدتر ارتقا می‌دهید:

  • هشدار مورد نیاز همچنان ظاهر می‌شود – بررسی عادی را ادامه دهید.
  • هشدار مورد نیاز ناپدید می‌شود – build را متوقف کنید؛ بررسی کنید که آیا نسخه جدید منطق پرس‌وجو را تغییر داده یا تغییر در کد باعث قطع جریان داده شده است.
  • مکان مثبت جدیدی ظاهر می‌شود – پس از تأیید اینکه یک مسیر تزریق واقعی است، آن را به لیست required اضافه کنید.
  • کنترل منفی شروع به هشدار دادن می‌کند – در استراتژی کاهش ریسک (mitigation) تجدیدنظر کنید؛ ممکن است قوانین سخت‌گیرانه‌تر شده باشند.

تحلیل استاتیک نمی‌تواند ثابت کند که مدل در زمان اجرا چه واکنشی نشان خواهد داد. مجموعه رگرسیون را با تست‌های خصمانه (adversarial tests) تکمیل کنید که واقعاً پرامپت‌های طراحی‌شده را به مدل ارسال کرده و پاسخ را تأیید می‌کنند.

نتیجه‌گیری

نسخه 2.26.0 CodeQL به شما این توانایی را می‌دهد که باگ‌های تزریق دستور را قبل از انتشار شناسایی کنید، اما تنها در صورتی که این توانایی را با یک فیکسچر رگرسیون متمرکز تثبیت کنید. با تعریف منابع غیرقابل اعتماد، مقصدهای واقعی SDK و انتظارات روشن در یک قرارداد JSON، شما یک قانون تحلیل استاتیک را به دروازه‌ای تبدیل می‌کنید که از رگرسیون‌ها جلوگیری کرده و توجه مداوم به سطح حمله در حال تکامل سریع را الزامی می‌کند.