فاز 19 · درس 26

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

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python (stdlib)
پیش‌نیاز
فاز 19 · درس 25 (گیت‌های اعتبارسنجی و بودجه‌ی مشاهده‌پذیری)، فاز 14 · درس 33 (دستورالعمل‌ها به عنوان قیدها)، فاز 14 · درس 38 (گیت‌های اعتبارسنجی)
یک ترمینال کامپیوتری محصور در یک مکعب شیشه‌ای محافظ با نرده‌های فلزی که از خروج مسیرها جلوگیری می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. معماری
  5. آنچه می‌سازید
  6. چرا این یک ساندباکس واقعی نیست؟
  7. اجرای کد
  8. ارتباط این درس با ادامه‌ی مسیر آموزشی

دادن دسترسی مستقیم شل به ایجنت‌های کدنویسی، مانند قرار دادن یک کاربر ناشناس پشت خط فرمان با دسترسی بالا است. حتی در پروژه‌های محلی، یک مدل تحت فشار ممکن است به اشتباه دستوراتی مثل rm -rf را اجرا کند، فایل‌های سیستمی حیاتی را دستکاری کند یا فرایندی بی‌پایان به راه بیندازد که سیستم را قفل کند. حفاظت از محیط توسعه در برابر این خطاهای ناشیانه، اولین گام پیش از سپردن کارهای واقعی به ایجنت‌ها است.

در این درس یاد می‌گیرید چگونه پیش از رسیدن به ایزوله‌سازی‌های سنگین کرنل، یک لایه‌ی محافظتی سبک و سریع مبتنی بر پایتون بسازید. این لایه با تکیه بر تحلیل ساختار آرگومان‌ها، مسدودسازی نام فایل‌های اجرایی پرخطر، حبس مسیرهای فایل درون ریشه‌ی پروژه (Path Jail) و قطع خودکار فرایندهای طولانی، امنیت پایه‌ی اجرای دستورات را تضمین می‌کند.

گیت اعتبارسنجی (verification gate) تصمیم می‌گیرد که آیا یک فراخوانی ابزار (tool call) باید اجرا شود یا نه. ساندباکس (sandbox) تعیین می‌کند وقتی اجرا شد، چه اتفاقی بیفتد. در این درس، یک اجراکننده‌ی زیرفرایند (subprocess runner) می‌سازیم که جلوی فایل‌های اجرایی خطرناک و آرگومان‌های با ساختار ناامن را می‌گیرد، تمام مسیرهای فایلی را در ریشه‌ی پروژه به زندان مسیر (path jail) می‌اندازد، خروجی‌های بیش‌ازحد حجیم را هرس (truncate) می‌کند و با اعمال یک مهلت زمانی بر اساس ساعت واقعی (wall-clock timeout)، فرایندهای مهارنشدنی را متوقف می‌سازد. این دومین لایه از دو لایه‌ای است که بین مدل و سیستم‌عامل قرار می‌گیرند.

اهداف یادگیری

  • پیاده‌سازی کلاس Sandbox به‌صورت پوششی دور subprocess.run همراه با مهلت زمانی، دریافت خروجی و هرس (کوتاه‌سازی).
  • جلوگیری از اجرای دستور بر اساس نام آن از طریق لیست سیاه (denylist) و بر اساس ساختار آن از طریق بازرس argv.
  • رد کردن هر آرگومان مسیری که مقصد نهایی‌اش خارج از ریشه‌ی تعیین‌شده‌ی پروژه باشد.
  • رد کردن نویسه‌های کنترلی شل (metacharacters) وقتی حالت شل غیرفعال است.
  • برگرداندن یک SandboxResult ساختاریافته که ابزارهای مشاهده‌پذیری و بستر آزمایشی (eval harness) بتوانند آن را دریافت و پردازش کنند.

صورت مسئله

یک ایجنت (agent) کدنویسی که دسترسی اجرای دستورات شل را داشته باشد، می‌تواند تنها در یک نوبت گفت‌وگو (turn) در پشتی (backdoor) بگذارد، کلیدها را به بیرون درز دهد، لپ‌تاپ توسعه‌دهنده را کاملاً از کار بیندازد و هزینه‌ی سرسام‌آوری روی دست حساب ابری بگذارد. کم‌هزینه‌ترین راه دفاع این است که اصلاً به آن شل ندهید. دومین راه کم‌هزینه، استفاده از ساندباکسی است که جلوی لیست دقیق و مشخصی از الگوها را می‌گیرد.

سه دسته از حالت‌های شکست (failure modes) مرتباً در ردگیری محاسبات (trace) ایجنت‌ها دیده می‌شوند:

دسته‌ی اول: فایل‌های اجرایی خطرناک. مدلی که برای حل یک مشکل مسیر تحت فشار قرار می‌گیرد، سراغ sudo، chmod -R 777، rm -rf، mkfs یا dd می‌رود. هیچ‌کدام از این‌ها جایی در اجرای یک ایجنت ندارند. لیست سیاه با بررسی نام اصلی و نام‌های مستعار جلوی آن‌ها را می‌گیرد.

دسته‌ی دوم: ترفندهای argv. مدلی که به آن گفته شده شل در کار نیست، حمله‌ی خود را از طریق یک مفسر هدایت می‌کند: دستوراتی مثل python3 -c "import os; os.system('rm -rf /')", bash -c '...', node -e '...' یا perl -e '...'. ساندباکس باید بداند اجرای هر مفسری با پرچمی مشابه -c، عملاً همان فراخوانی شل با یک مرحله‌ی اضافه است.

دسته‌ی سوم: گریز از مسیر (path escape). به مدل گفته می‌شود فایل ./src/main.py را بخواند، اما در عوض سراغ ../../etc/passwd می‌رود. ساندباکس با پیدا کردن مسیر نهایی از طریق os.path.realpath و بررسی پیشوند، تک‌تک آرگومان‌های مسیر را در یک زندان مسیر حبس می‌کند.

این ساندباکس به معنای سیستم‌عاملی کلمه یک مرز امنیتی نیست. یک مهاجم مصمم با دسترسی اجرای کد همچنان می‌تواند از آن عبور کند. این ساندباکس یک گاردریل (guardrail) در زمان توسعه است: حالت‌های شکست رایج را با هیاهو و به‌صورت صریح آشکار می‌کند و مانع از آن می‌شود که ناشی‌گری و خطای محض ایجنت آسیبی بزند.

مفاهیم پایه

ToolCallقبلاً از زنجیره گیت عبور کردهSandbox.run()1. بررسی فایل اجرایی در برابر لیست سیاهrm, sudo, mkfs, ...2. بازرسی argvinterpreter -c، نویسه‌های کنترلی شل وقتیshell=False3. بررسی آرگومان‌های مسیرماننددر برابر project_root از طریق realpath4. ایجاد زیرفرایندضبط خروجی، مهلت زمانی واقعی، پاکسازیenv5. هرس stdout/stderr تا سقفmax_output_bytesSandboxResultexit_code, stdout, stderr,truncated, timed_out, denied, reason

این ساندباکس چهار محور رد کردن دارد: نام، argv، مسیر و ساختار. هر محور یک تابع خالص (pure function) بر پایه‌ی مشخصات فراخوانی است؛ پیش از آنکه هنوز زیرفرایندی اجرا شده باشد. زیرفرایند فقط زمانی متولد می‌شود که فراخوانی از سد هر چهار محور بگذرد.

کدهای خروج در SandboxResult همان کدهای مرسوم هستند: 0 برای موفقیت، عددی غیر از صفر برای شکست، به‌علاوه‌ی سه کد نشانگر برای حالت‌های رد شده (-100)، پایان مهلت زمانی (-101) و هرس‌شده (در این حالت کد خروج واقعی حفظ می‌شود و فقط یک پرچم مقدار می‌گیرد). درس‌های بعدی به‌جای پارس کردن متن stderr، مستقیماً از این نتیجه‌ی ساختاریافته استفاده می‌کنند.

زندان مسیر + لیست مسدودسازیامتناع با پیشوند، نام و ساختار آرگومان
PATH JAIL + DENYLISTTwo refusal surfaces sit between the model and the operating system. Every path argument resolves through realpath and must keep the project root as a prefix, so a ../../ traversal bounces off the jail wall instead of reaching /etc. Alongside it, a denylist refuses executables by name and an argv inspector catches interpreters smuggling shell through -c flags. Output is truncated and a wall-clock timeout kills whatever runs away.اعمال realpath بر تمام آرگومان‌ها · بررسی پیشوندساندباکسsubprocess.runزندان ریشهٔ پروژهsrc/main.pytests/✓ درون ریشه../../etc/passwd✗ خروج از ریشهsudorm -rfpython3 -cلیست مسدودسازیبر اساس نام و ساختار argv
دو لایه امتناع بین مدل و سیستم‌عامل قرار دارند. هر آرگومان مسیر از طریق realpath باز می‌شود و حتماً باید ریشهٔ پروژه را به عنوان پیشوند حفظ کند؛ بنابراین پیمایش با ../.. به جای رسیدن به etc/ به دیوارهٔ زندان برخورد می‌کند. در کنار آن، یک لیست مسدودسازی مانع از اجرای برنامه‌ها بر اساس نام می‌شود و یک بازرس argv مفسرهایی را که قصد دارند پوسته را با فلگ -c دور بزنند شناسایی می‌کند. خروجی کوتاه می‌شود و یک مهلت زمانی بر اساس ساعت واقعی هر فرایند سرکشی را متوقف می‌سازد.

معماری

فراخوانیاجراAgentHarnessدرس‌های 20-25ساندباکسلیست سیاهزندان مسیربازرسی argvمهلت زمانیهرسsubprocess.PopenSandboxResult

لیست سیاه یک frozenset از نام پایه‌ی (basename) فایل‌های اجرایی است. نام‌های مستعار (/bin/rm، /usr/bin/rm) همگی به یک نام پایه می‌رسند. بازرس argv ساختار مفسرها را می‌شناسد: هر argv که عنصر argv[0] آن یک مفسر باشد و آرگومان‌های بعدی با -c یا -e شروع شوند، رد خواهد شد. همچنین اگر در فراخوانی صراحتاً شل درخواست نشده باشد، دیدن نویسه‌های کنترلی شل (;, |, &, >, <, بک‌تیک‌ها و $()) باعث رد دستور می‌شود.

زندان مسیر ظریف‌ترین بخش کار است. ساندباکس در زمان ساخته شدن یک project_root تحویل می‌گیرد. هر آرگومانی که شبیه مسیر باشد (شامل / باشد یا با یک فایل موجود هم‌نام باشد)، با os.path.realpath نرمال‌سازی می‌شود و سپس با realpath ریشه‌ی پروژه مقایسه خواهد شد. اگر مقصد نهایی زیرمجموعه‌ی پوشه‌ی ریشه نباشد، دستور رد می‌شود. تلاش برای فرار از طریق پیوند نمادین (symlink) — یعنی وجود یک پیوند نمادین درون ریشه‌ی پروژه که به بیرون اشاره می‌کند — با بررسی مسیر واقعی (realpath) و نه مسیر ظاهری، خنثی می‌شود.

آنچه می‌سازید

پیاده‌سازی شامل فایل main.py و یک پوشه‌ی تست است:

  1. کلاس داده‌ی SandboxResult: شامل exit_code، stdout، stderr، truncated، timed_out، denied، reason و duration_ms.
  2. کلاس داده‌ی SandboxConfig: شامل project_root، max_output_bytes، timeout_seconds، denylist و interpreter_block.
  3. کلاس Sandbox: متد run(argv, *, shell=False, cwd=None) که یک SandboxResult برمی‌گرداند.
  4. توابع کمکی داخلی برای رد دستور: _check_executable_denylist، _check_argv_interpreter، _check_shell_metachars و _check_path_jail.
  5. هرس خروجی با پرچم صریح truncated و قرار دادن یک خط نشانه‌گذاری درون متن دریافتی.
  6. یک بخش نمایشی در انتهای فایل: اجرای دنباله‌ای از فراخوانی‌های مجاز و مخرب به همراه نمایش خروجی هر کدام.

این ساندباکس به‌طور پیش‌فرض از متد subprocess.run با گزینه‌های shell=False و capture_output=True استفاده می‌کند. مهلت زمانی بر اساس ساعت واقعی با آرگومان timeout اعمال می‌شود؛ در صورت بروز خطای TimeoutExpired، ساندباکس کل گروه فرایند را می‌کشد و یک شیء ساختاریافته‌ی SandboxResult می‌سازد.

چرا این یک ساندباکس واقعی نیست؟

ساندباکس این درس از قابلیت‌های namespaces، cgroups، seccomp، gVisor، Firecracker یا هیچ ابزار ایزوله‌سازی دیگری در سطح کرنل (kernel) استفاده نمی‌کند. هر کاری که زیرفرایند بتواند انجام دهد، ساندباکس هم می‌تواند انجام دهد. حفاظت در اینجا ساختاری است: جلوی رایج‌ترین دستورات خطرناک ایجنت گرفته می‌شود و این امتناع با صدای بلند در سیستم‌های مشاهده‌پذیری ثبت می‌شود، به‌جای آنکه بی‌صدا اجرا شود.

برای ایجنت‌های محیط عملیاتی (production)، لایه‌های بیشتری روی این ساختار اضافه می‌کنید: اجرا درون یک کانتینر Docker بدون دسترسی‌های ویژه، اجرا در یک میکرو ماشین مجازی (microVM)، حذف قابلیت‌های سیستمی (drop capabilities)، مانت کردن ریشه‌ی پروژه به‌صورت فقط‌خواندنی و یک پوشه‌ی موقت به‌صورت خواندنی-نوشتنی، تعیین ulimit روی رم و پردازنده، و پالایش متغیرهای محیطی با یک لیست مجاز کاملاً مطمئن. در درس 29 بخشی از این موارد را پیاده می‌کنیم. ایزوله‌سازی در سطح سیستم‌عامل خارج از دامنه‌ی (scope) این درس است.

اجرای کد

cd phases/19-capstone-projects/26-sandbox-runner-denylist
python3 code/main.py
python3 -m pytest code/tests/ -v

بخش نمایشی یک پوشه‌ی موقت می‌سازد، یک فایل سالم داخل آن می‌گذارد و سپس مجموعه‌ای از دستورات مختلف را اجرا می‌کند. فراخوانی‌های قانونی موفق می‌شوند. موارد ردشده یک شیء SandboxResult با مقدار denied=True و دلیل مشخص برمی‌گردانند. در صورت اتمام زمان، پرچم timed_out=True فعال می‌شود و برای خروجی‌های بیش‌ازحد بزرگ، پرچم truncated=True ست خواهد شد. در پایان، دمو یک جدول JSON از نتایج چاپ می‌کند و با کد صفر خارج می‌شود.

ارتباط این درس با ادامه‌ی مسیر آموزشی

درس 25 زنجیره‌ی گیت‌ها را پیاده کرد. درس 26 همان مجری‌ای است که پس از صدور مجوز ALLOW از سوی گیت اجرا می‌شود. بستر آزمایشی درس 27 نتایج ساندباکس را با کدهای خروج موردانتظار برای هر وظیفه (task) مقایسه خواهد کرد. در درس 28، یک اسپن (span) با نام gen_ai.tool.execution دور هر فراخوانی Sandbox.run ثبت می‌کنیم. در نهایت، دموی سرتاسری (end-to-end) درس 29 یک ایجنت کدنویسی واقعی را با هر دو لایه متصل خواهد کرد.

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسپیش از خواندن درس: مهم‌ترین دلیل بررسی شکل و ساختار argv در ساندباکس برای اجرای مفسرهایی نظیر python3 -c چیست؟
  2. حین درسساندباکس با project_root=/work پیکربندی شده است. آرگومان مسیری به‌صورت ‎./src/main.py‎ ارسال می‌شود. بررسی زندان مسیر چگونه انجام می‌گیرد؟
  3. حین درسیک SandboxResult با مقادیر denied=True، timed_out=False و exit_code=-100 بازمی‌گردد. تفسیر استاندارد آن چیست؟
  4. حین درسچرا ساندباکس به طور پیش‌فرض shell=False را ترجیح می‌دهد و نویسه‌های خاص شل را رد می‌کند؟
  5. پس از درسمی‌خواهید محدودیت حافظه به ازای هر فرایند را به ساندباکس اضافه کنید. ساندباکس این درس این را پیاده‌سازی نکرده است. اقدام صحیح بعدی چیست؟
  6. پس از درسمدل ساندباکس را با آرگومان ['cat', 'src/../src/main.py'] فراخوانی می‌کند. این فایل زیر project_root وجود دارد. آیا این فراخوانی باید از زندان مسیر عبور کند؟

سؤالات متداول

چرا این ساندباکس یک مرز امنیتی کامل در سطح سیستم‌عامل نیست؟

این ساندباکس از قابلیت‌های سطح هسته مثل cgroups، namespaces یا seccomp استفاده نمی‌کند و فقط الگوهای مخرب و خطاهای رایج ایجنت را مسدود می‌کند؛ در محیط پروداکشن باید آن را درون Docker یا microVM قرار داد.

زندان مسیر (Path Jail) چگونه جلوی فرار ایجنت با Symlink را می‌گیرد؟

ساندباکس مسیرها را از طریق os.path.realpath نرمال‌سازی می‌کند تا مسیر فیزیکی نهایی را بررسی کند، بنابراین اگر یک پیوند نمادین در پروژه به بیرون اشاره کند بلافاصله رد می‌شود.

ساندباکس چگونه با ترفندهای اجرای دستور از طریق مفسرها مقابله می‌کند؟

بازرس آرگومان‌ها در صورتی که اولین پارامتر یک مفسر (مثل python3 یا bash) باشد و آرگومان‌های بعدی با پرچم‌هایی نظیر -c یا -e شروع شوند، دستور را معادل فراخوانی شل شناسایی کرده و آن را رد می‌کند.

ما رو توی گوگل بیشتر ببین

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