یک چت‌بات پشتیبانی تیکتی را می‌خواند که کاربر فرستاده است. داخل تیکت، در میان توضیح یک مشکل معمولی، چند جمله نشسته که مخاطبشان کاربر نیست؛ مخاطبشان مدل است. اگر آن چند جمله کار کند، چت‌بات چیزی را می‌گوید یا انجام می‌دهد که سازنده‌اش برایش ننوشته بود.

این حمله شناخته‌شده است و بیشتر تیم‌ها برایش فیلتر گذاشته‌اند. چیزی که کمتر دیده می‌شود این است که فیلترها معمولاً روی متن انگلیسی تنظیم شده‌اند، و فارسی به مدل اجازه می‌دهد همان دستور را بفهمد در حالی که فیلتر متن دیگری می‌بیند.

تزریق پرامپت در دو پاراگراف

مدل زبانی دستور سیستمی، پیام کاربر، سند بازیابی‌شده و خروجی ابزارها را یک‌جا و به شکل یک جریان متن می‌گیرد. راه مطمئنی ندارد که بفهمد کدام بخش دستور است و کدام بخش فقط شبیه دستور. هر کسی که بتواند متنی جلوی مدل بگذارد، می‌تواند برای هدایت آن تلاش کند.

پس «کاربر» تنها کسی نیست که باید نگرانش بود. نویسنده‌ی هر صفحه‌ی وبی که برنامه می‌خواند، هر PDF که آپلود می‌شود، هر تیکت و هر ایمیل، همگی می‌توانند متن وارد کنند. این حالت دوم را تزریق غیرمستقیم می‌نامند و در عمل خطرناک‌تر است، چون قربانی هیچ کار مشکوکی انجام نمی‌دهد. روش کلی آزمون چنین برنامه‌هایی را پیش‌تر در راهنمای تست نفوذ برنامه‌های مبتنی بر LLM نوشته‌ایم؛ این نوشته روی یک لایه‌ی مشخص تمرکز دارد: خودِ متن.

متن از کجا به مدل می‌رسد پیام کاربر نام فایل آپلودشده سند بازیابی‌شده عنوان تیکت پاسخ یک API بیرونی پیام خطای سامانه‌ی دیگر زمینه‌ی مدل یک جریان متن، بی‌مرز میان دستور و داده مدل دستورِ پنهان را هم می‌خواند ابزارها ایمیل، رکورد، پرداخت مسیر اول، تزریق مستقیم است؛ بقیه غیرمستقیم‌اند و هر کدام یک مورد آزمون.
هر مسیری که به زمینه‌ی مدل می‌رسد جای آزمون است، نه فقط پیام کاربر.

چرا فارسی فرق می‌کند

فیلترها معمولاً رشته‌ها را جست‌وجو می‌کنند یا ورودی را با یک مدل کوچک‌تر دسته‌بندی می‌کنند. هر دو روش به این وابسته‌اند که متنِ رسیده همان شکلی باشد که موقع تنظیم فیلتر تصور می‌شد. فارسی این فرض را به چند شکل می‌شکند، و نکته‌ی مهم این است که هیچ‌کدام از این‌ها معنای جمله را برای مدل عوض نمی‌کند:

یک حرف، دو کد. «ی» فارسی و «ي» عربی دو نویسه‌ی متفاوت با دو کد متفاوت‌اند و در بیشتر قلم‌ها شبیه هم دیده می‌شوند. همین برای «ک» و «ك» هم درست است. فهرستی که با یکی نوشته شده، نسخه‌ی دیگر را نمی‌بیند.

فاصله‌هایی که دیده نمی‌شوند. نیم‌فاصله در فارسی یک نویسه‌ی واقعی است و متن بدون آن ناخوانا می‌شود، بنابراین هیچ برنامه‌ای نمی‌تواند به‌سادگی حذفش کند. کنار آن چند نویسه‌ی عرض‌صفر دیگر هم وجود دارد که اصلاً دیده نمی‌شوند. یک کلمه با یکی از این‌ها در میانش، برای چشم همان کلمه است و برای جست‌وجوی رشته‌ای کلمه‌ای دیگر.

سه دستگاه رقم. ارقام فارسی، ارقام عربی و ارقام لاتین هر سه در متن فارسی ظاهر می‌شوند. قاعده‌ای که روی یکی نوشته شده، بقیه را رد می‌کند.

فینگلیش. نوشتن فارسی با حروف لاتین در ایران رایج است و مدل‌های بزرگ آن را می‌فهمند. برای فیلتری که فهرست کلمات فارسی دارد، این متن اصلاً فارسی نیست.

نویسه‌های جهت‌دهی. یونی‌کد نویسه‌هایی دارد که جهت نمایش متن را عوض می‌کنند. با آن‌ها می‌توان کاری کرد که آنچه یک کارشناس در پنل پشتیبانی می‌خواند با آنچه مدل دریافت می‌کند یکی نباشد. این همان خانواده‌ای است که در کد منبع به نام Trojan Source شناخته شد و در متن هم به همان اندازه کار می‌کند.

دو کلمه که روی صفحه یکی دیده می‌شوند، برای فیلتر دو رشته‌ی متفاوت‌اند:

denylist = ["کیک"]            # U+06A9 U+06CC U+06A9: Persian kaf, Persian yeh
text     = "كيك"              # U+0643 U+064A U+0643: Arabic kaf, Arabic yeh
text in denylist              # False: the same on screen, different code points
normalise(text) in denylist   # True, once Arabic letters are mapped to Persian
همان دستور، به این شکل فیلتر فهرست کلمات مدل به فارسی ساده جلویش را می‌گیرد به آن نمی‌رسد با «ي» عربی به جای «ی» فارسی نمی‌شناسد دستور را می‌فهمد با یک نویسه‌ی عرض‌صفر وسط کلمه نمی‌شناسد دستور را می‌فهمد به فینگلیش، فارسی با حروف لاتین نمی‌شناسد دستور را می‌فهمد
برای مدل هر چهار شکل یک معنا دارند؛ فیلتر فهرست کلمات فقط اولی را می‌شناسد.

شکل‌های آزمون

جدول زیر شکلِ کار را توضیح می‌دهد، نه نمونه‌ی آماده. هدف این است که تیم بتواند خودش مجموعه‌ی آزمون بسازد.

شکل کاری که با متن می‌کند چه چیزی را رد می‌کند
جایگزینی نویسه‌های هم‌شکل «ی» و «ک» را با هم‌تایان عربی‌شان عوض می‌کند فهرست کلمات، قاعده‌های رشته‌ای
درج نویسه‌ی عرض‌صفر کلمه را از میان می‌شکند بدون تغییر ظاهر تطبیق دقیق رشته، بخشی از دسته‌بندها
تغییر دستگاه ارقام ارقام را میان فارسی، عربی و لاتین جابه‌جا می‌کند قاعده‌هایی که روی یک دستگاه نوشته شده‌اند
فینگلیش همان جمله را با حروف لاتین می‌نویسد فیلترهای واژگانی فارسی
آمیختن خط چند حرف لاتین هم‌شکل را داخل کلمه‌ی فارسی می‌گذارد تطبیق رشته و گاهی نرمال‌سازی ناقص
نویسه‌های جهت‌دهی نمایش را از محتوای واقعی جدا می‌کند بازبینی انسانی و لاگ‌هایی که خام نمایش می‌دهند
تزریق غیرمستقیم متن را در سندی می‌گذارد که برنامه بعداً می‌خواند هر کنترلی که فقط روی پیام کاربر نشسته است

هر سطر یک متغیر است. ارزش آزمون وقتی است که هر بار فقط یکی از آن‌ها را عوض کنید، وگرنه معلوم نمی‌شود کدام تغییر جواب داده است.

آزمون یک چت‌بات، گام به گام

۱. مسیرهای ورود متن را فهرست کنید. پیام کاربر یکی از آن‌هاست. نام فایل آپلودشده، محتوای سندی که بازیابی می‌شود، عنوان تیکت، پاسخ یک API بیرونی و حتی پیام خطای سامانه‌ای دیگر هم متن‌اند. هر مسیری که در این فهرست نیاید، آزمون نمی‌شود.

۲. یک خط مبنا بگیرید. ساده‌ترین شکل دستور را به زبان فارسی و بدون هیچ پوششی بفرستید. اگر همین کار کرد، کار تمام است و بقیه‌ی گام‌ها لازم نیست. اگر نکرد، حالا می‌دانید فیلتر چه چیزی را می‌گیرد.

۳. یک متغیر را عوض کنید. همان جمله را با یکی از شکل‌های جدول بازنویسی کنید. نتیجه را ثبت کنید. بعد به حالت اول برگردید و متغیر بعدی را امتحان کنید.

۴. سراغ مسیر غیرمستقیم بروید. متن را در سندی بگذارید که برنامه بازیابی می‌کند، نه در پیامی که خودتان می‌فرستید. بسیاری از برنامه‌ها ورودی کاربر را جدی می‌گیرند و محتوای بازیابی‌شده را بی‌خطر فرض می‌کنند.

۵. بپرسید مدل چه کاری می‌تواند بکند. اینکه مدل جمله‌ای بگوید که نباید، یک چیز است؛ اینکه ابزاری را صدا بزند، رکوردی را عوض کند یا داده‌ای را بیرون بفرستد چیز دیگری است. شدت یافته را دسترسی مدل تعیین می‌کند، نه لحن پاسخش.

۶. تکرار کنید و بشمارید. رفتار مدل احتمالاتی است. یک تلاش موفق ثابت نمی‌کند مشکل همیشه هست و یک تلاش ناموفق ثابت نمی‌کند نیست. هر آزمون را چند بار اجرا کنید و نتیجه را به شکل «از هر ده بار، سه بار» گزارش کنید.

۷. لاگ‌ها را نگاه کنید. اگر متن تزریق‌شده در لاگ‌ها پیدا نشود، یا آن‌قدر پاک‌سازی شده باشد که شکل اصلی‌اش معلوم نباشد، تیم امنیت بعداً نمی‌تواند حمله‌ای را که اتفاق افتاده بازسازی کند. این خودش یک یافته است.

لاگی که هر دو شکل متن را نگه دارد، حمله را بعداً قابل دیدن می‌کند:

2026-09-21T10:14:03Z chat msg=48213 source=ticket.subject
  raw        = "\u0643\u064A\u0643 …"
  normalised = "کیک …"
  filter     = pass    tool_calls = 0

نمونه‌ی یک یافته

بخش محتوا
عنوان دور زدن فیلتر ورودی با «ي» عربی
مسیر ورود پیام گفت‌وگو؛ همین متن در عنوان تیکت هم کار کرد
شکل جایگزینی نویسه‌ی هم‌شکل، با تغییر یک متغیر
نرخ موفقیت ۷ بار از ۱۰ تلاش، با ثبت نسخه‌ی مدل و تاریخ
اثر مدل دستور سیستمی‌اش را فاش کرد؛ ابزاری فراخوانی نشد
شدت متوسط: مدل به ابزاری که داده را تغییر دهد دسترسی نداشت
علت ریشه‌ای فیلتر متن خام را مقایسه می‌کند؛ در مرز ورودی نرمال‌سازی‌ای نیست
اصلاح نرمال‌سازی یک‌باره در مرز، فیلتر روی نسخه‌ی نرمال‌شده، نگه‌داشتن نسخه‌ی اصلی در لاگ
آزمون دوباره همان ۱۰ تلاش پس از اصلاح: بدون موفقیت

دفاع: اول نرمال‌سازی، بعد فیلتر

بیشتر تیم‌ها فیلتر را به متنی می‌دهند که همان شکلی رسیده که کاربر فرستاده. ترتیب درست برعکس است.

متن را پیش از هر تصمیمی یک‌بار نرمال کنید: نویسه‌های عربی را به فارسی نگاشت کنید، نویسه‌های عرض‌صفر و جهت‌دهی را حذف یا آشکار کنید، ارقام را یکدست کنید و فرم یونی‌کد را یکسان کنید. سپس فیلتر و دسته‌بند را روی همین متنِ نرمال‌شده اجرا کنید. نسخه‌ی اصلی را برای نمایش به کاربر نگه دارید تا چیزی از معنای نوشته‌ی او کم نشود.

ترتیب رایج متن کاربر فیلتر مدل فیلتر شکلِ خام را مقایسه می‌کند و مدل معنا را می‌خواند؛ هر نویسه‌ی هم‌شکل یا نامرئی راهی از کنار فیلتر است. ترتیبی که دوام می‌آورد متن کاربر نرمال‌سازی یک بار، در مرز فیلتر و دسته‌بند روی نسخه‌ی نرمال مدل نسخه‌ی اصلی، دست‌نخورده، برای نمایش و لاگ نویسه‌های عربی ← فارسی عرض‌صفر و جهت‌دهی ← حذف یا آشکار ارقام ← یک دستگاه
نرمال‌سازی حمله را حذف نمی‌کند؛ ارزان‌ترین راه دور زدن فیلتر را می‌بندد.

این کار را یک بار و در مرز ورودی انجام دهید. اگر هر بخش از برنامه نرمال‌سازی خودش را داشته باشد، اختلاف میان آن‌ها دوباره همان شکافی می‌شود که دنبالش بودیم.

نکته: نیم‌فاصله را حذف نکنید؛ بخشی از املای درست فارسی است. فقط نویسه‌های نامرئی‌ای را بردارید که در متن کاری ندارند، و بقیه را به‌جای حذف، برای بازبین آشکار کنید.

نرمال‌سازی حمله را از بین نمی‌برد. فقط جلوی ارزان‌ترین راه دور زدن را می‌گیرد و باعث می‌شود فیلتری که نوشته‌اید همان چیزی را ببیند که مدل می‌بیند.

کنترل‌هایی که به مدل وابسته نیستند

هر دفاعی که به درست رفتار کردن مدل تکیه کند، روزی که مدل به‌روز شود ممکن است رفتار دیگری نشان دهد. کنترل‌های دوام‌آور بیرون از مدل می‌نشینند:

  • کمترین دسترسی برای ابزارها. مدل فقط به کاری دسترسی داشته باشد که برای وظیفه‌اش لازم است، با فهرست مجاز، نه با اعتماد عمومی.
  • تأیید انسانی برای کار برگشت‌ناپذیر. حذف، پرداخت و ارسال به بیرون از سازمان نباید با یک جمله در یک تیکت انجام شود.
  • خروجی مدل ورودی نامطمئن است. هر جا که خروجی به مرورگر، پرس‌وجو یا فرمان می‌رسد، همان‌طور با آن رفتار کنید که با ورودی کاربر.
  • جداسازی سطح اعتماد در بازیابی. سندی که کاربر آپلود کرده با سند داخلی تأییدشده یکی نیست؛ برنامه باید این تفاوت را بداند.
  • سقف و پایش. محدودیت نرخ روی فراخوانی ابزار، و هشدار وقتی الگوی استفاده از حالت معمول خارج می‌شود.

پرسش‌هایی برای تیم خودتان یا تأمین‌کننده

پرسش چرا مهم است
فیلتر روی متن نرمال‌شده اجرا می‌شود؟ وگرنه یک «ي» عربی از کنارش رد می‌شود
جز پیام کاربر، چه متنی به مدل می‌رسد؟ تزریق غیرمستقیم از همین مسیرها می‌آید
مدل بدون تأیید یک انسان چه کاری می‌تواند بکند؟ شدت هر یافته از همین دسترسی می‌آید
متن تزریق‌شده را با شکل اصلی‌اش در لاگ پیدا می‌کنیم؟ بدون آن، حمله را بعداً نمی‌شود بازسازی کرد

پرسش‌های متداول

آیا فیلتر ورودی کافی است؟ نه. فیلتر لایه‌ی اول است و کارش بالا بردن هزینه‌ی حمله‌ی ساده است. اگر تنها دفاع باشد، دیر یا زود یکی از شکل‌های بالا از آن رد می‌شود.

مدلی که فارسی را بهتر می‌فهمد امن‌تر است؟ لزوماً نه. فهم بهتر زبان یعنی دستور پوشیده‌شده را هم بهتر می‌فهمد. مسئله جای دیگری است: اینکه برنامه چقدر به خروجی مدل اعتماد می‌کند و مدل به چه چیزی دسترسی دارد.

تفاوت تزریق مستقیم و غیرمستقیم در گزارش چیست؟ در مستقیم، مهاجم باید با برنامه حرف بزند. در غیرمستقیم کافی است متنی را جایی بگذارد که برنامه بعداً می‌خواند و قربانی کار عجیبی نمی‌کند. دومی معمولاً شدت بالاتری می‌گیرد.

وقتی حمله همیشه جواب نمی‌دهد چطور گزارش کنیم؟ با نرخ. تعداد تلاش و تعداد موفقیت را بنویسید، همراه با نسخه‌ی مدل و تاریخ آزمون، چون همین دو با به‌روزرسانی عوض می‌شوند.

آیا نرمال‌سازی متن کاربر را خراب می‌کند؟ اگر نسخه‌ی نرمال‌شده را فقط برای تصمیم‌گیری به کار ببرید و نسخه‌ی اصلی را برای نمایش و ذخیره نگه دارید، نه.

اگر چت‌بات، دستیار داخلی یا عاملی دارید که متن از بیرون می‌خواند، همین فهرست مسیرهای ورود را یک بار با تیم خودتان پر کنید. معمولاً یکی دو مسیر پیدا می‌شود که کسی تا امروز به آن فکر نکرده بود.