Системний промпт як точка атаки: простежуємо небезпечний потік даних

безпека застосунківстатичний аналізвеликі мовні моделі

Практичний розбір того, як недовірені дані потрапляють у системний промпт, як підтвердити знахідку статичним аналізом і як виправити межу довіри

Зміна, що виглядає цілком розумною

Колега просить: «Додай опис звернення прямо в інструкцію, щоб відповідь була точнішою». Розробник змінює функцію так:

const BASE_SYSTEM = "Summarize support tickets for an operator.";

function addContext(base: string, value: string) {
  return `${base}\nTicket context:\n${value.trim()}`;
}

export async function summarize(req: Request) {
  const ticket = req.body.ticket;
  const system = addContext(BASE_SYSTEM, ticket);

  return llm.generate({
    system,
    user: "Prepare a short summary."
  });
}

Код не схожий на класичний злам. Проте ticket контролює користувач, а його текст стає частиною інструкції найвищого рівня. Обрізання пробілів нічого не змінює в моделі довіри.

Малюємо маршрут source → propagation → sink

Для цього фрагмента маршрут короткий:

  1. req.body.ticket позначаємо як джерело.
  2. Виклик addContext, шаблонний рядок і trim() утворюють поширення.
  3. Поле system у виклику клієнта мовної моделі є точкою призначення.

Саме такий шлях важливіший за пошук слів system або prompt. Назви змінних можна змінити, а дані можуть пройти через кілька модулів, об’єктів і допоміжних функцій. Ризик визначає фактичний потік.

Ручна перевірка як короткий runbook

Спочатку складіть перелік викликів бібліотек OpenAI, Anthropic, Google GenAI та інших клієнтів мовних моделей. Для кожного виклику зафіксуйте, де створюються системні повідомлення.

Далі перевірте кожне вставлене значення:

  • чи походить воно з HTTP-запиту, форми, файла або зовнішнього API;
  • чи може його змінити користувач, постачальник або інша система;
  • через які функції воно проходить;
  • у якій ролі зрештою надсилається моделі.

Межа довіри проходить не лише біля вебформи. Вміст завантаженого документа або відповідь партнерського API також можуть бути недовіреними.

Що додає SAST і як розбирати результат

У CodeQL 2.26.0 з’явився запит js/system-prompt-injection для JavaScript і TypeScript. Він є актуальним прикладом того, як SAST простежує дані до відомих системних точок призначення. У релізі також розширено моделювання викликів OpenAI, Anthropic і Google GenAI.

Перед запуском перевірте версію аналізатора та набір запитів. Після сканування відкрийте трасу кожної знахідки й підтвердьте джерело, поширення та кінцеву роль повідомлення.

Не закривайте результат як хибне спрацювання лише через trim(), видалення HTML або ліміт довжини. Виправданим винятком може бути значення, яке вибирається на сервері з фіксованого дозволеного набору й не містить введений текст. Це має бути видно з коду та тестів.

Перебудовуємо межу довіри

Базове виправлення — залишити системну інструкцію статичною, а сторонній текст передавати як користувацький вміст:

function buildMessages(ticket: string) {
  return [
    { role: "system", content: "Summarize support tickets for an operator." },
    { role: "user", content: `Ticket to summarize:\n${ticket}` }
  ];
}

Якщо потрібен динамічний стиль, перетворюйте короткий ідентифікатор на серверне значення через дозволений список. Не вставляйте сире поле style в інструкцію. Антипатерни — конкатенація, приховане змішування ролей у допоміжній функції та припущення, що екранування робить природну мову довіреною.

Негативний тест

Тест має перевіряти архітектурну властивість, а не намагатися довести повну безпечність моделі:

it("keeps untrusted ticket text out of the system message", () => {
  const marker = "IGNORE PRIOR INSTRUCTIONS";
  const messages = buildMessages(marker);

  expect(messages.find(m => m.role === "system")?.content)
    .not.toContain(marker);
  expect(messages.find(m => m.role === "user")?.content)
    .toContain(marker);
});

Такий тест захищає виправлену структуру під час рефакторингу. Окремі поведінкові перевірки потрібні для реальних відповідей моделі та інструментів.

Поступове впровадження перевірки

Почніть з інвентаризації викликів і створіть базовий зріз. Потім вручну розберіть результати, виправте підтверджені потоки та задокументуйте вузькі винятки. Наступний крок — неблокувальна перевірка pull request, яка показує нові знахідки. Блокування варто додавати лише після стабілізації правил і відповідальності за triage.

SAST не бачить усі складені під час виконання повідомлення й не оцінює поведінку моделі. Тому потрібні моделювання загроз, мінімальні права інструментів, авторизація кожної дії, перевірка вихідних даних, журналювання та runtime-моніторинг. Розділення ролей зменшує небезпечний потік, але не є повним захистом від prompt injection.

Джерела

Короткий чеклист

  • знайти всі місця формування системних повідомлень
  • позначити входи з HTTP-запитів, файлів і зовнішніх API
  • простежити кожне значення до виклику мовної моделі
  • винести недовірений вміст із системного повідомлення
  • додати негативний тест для ролей повідомлень
  • запроваджувати блокування лише після ручного розбору базового зрізу

Перевірка потоку даних до системного промпту

Допоможи перевірити функцію на небезпечне потрапляння недовірених даних у системний промпт. Спочатку попроси надати: 1. Фрагмент JavaScript або TypeScript із викликом мовної моделі. 2. Перелік можливих входів: HTTP-запити, форми, файли, зовнішні API, база даних. 3. Назву бібліотеки та версію, якщо вони відомі. 4. Очікувану роль кожного повідомлення: system, user або tool. 5. Наявні перевірки, дозволені значення та тести. Поверни результат у форматі: - Карта потоку: source → propagation → sink. - Рівень довіри для кожного входу з коротким обґрунтуванням. - Підтверджені ризики та можливі хибні спрацювання окремими списками. - Мінімальне виправлення з прикладом коду. - Негативний регресійний тест. - Додаткові runtime-заходи, яких статичний аналіз не перевіряє. Не вважай очищення пробілів, екранування HTML або обмеження довжини доказом безпечності. Не стверджуй, що зміна ролі повідомлення повністю усуває prompt injection.