تصور کنید یک پیوند نمادین ساده، یک عامل هوش مصنوعی را مستقیماً از حصار امنیتی پوشههای مجاز خارج کند و تمام گاردهای مسیر را بیاثر سازد. این موضوع در تحلیل فنی منتشر شده در ۲۵ اوت ۲۰۲۶ فاش شد؛ یافتههایی که نشان میدهد اکثر سیستمهای حفاظتی به جای تحلیل سطح سیستمفایل، بر نرمالسازی متنی مسیرها تکیه میکنند و همین امر یک حفره امنیتی خاموش ایجاد کرده است.
بسیاری از توسعهدهندگان برای اطمینان از اینکه یک عامل (Agent) — شبیه به دستیاری که اجازه دارد فقط در یک اتاق خاص کار کند — تنها در پوشهای مشخص مانند /project/products/ بنویسد، از توابع بررسی محتوا استفاده میکنند. یک پیادهسازی رایج به این شکل است:
import { isAbsolute, relative, resolve } from "node:path";
function contains(root, target) {
const rel = relative(resolve(root), resolve(target));
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}
در این روش، برنامهنویسان از دستوراتی مثل path.resolve و path.relative استفاده میکنند تا حملات پیمایش دایرکتوری (Directory Traversal) مانند ../../etc/passwd را مسدود کنند. این منطق تلاشهای آشکار برای خروج از ریشه را متوقف میکند، اما درک نمیکند که سیستمعامل واقعاً چگونه مکان فایلها را مدیریت میکند.
همانطور که در تحلیل قبلی ما دربارهی امنیت مدلهای بازمتن اشاره کردیم، اعتماد به لایههای نرمافزاری بدون در نظر گرفتن لایههای زیرساختی همیشه ریسکپذیر است. حالا فرض کنید عاملی فقط اجازه دسترسی به پوشه products/ را دارد. اگر یک پیوند نمادین (Symlink) — که مثل یک تابلوی راهنماست و شما را از یک نقطه به جای دیگری در شهر میبرد — به نام cache در این پوشه ایجاد شود و به پوشه حساس secrets/ در جای دیگری از دیسک اشاره کند، گارد امنیتی دور زده میشود. وقتی عامل فایلی را در products/cache/notes.txt مینویسد، گارد امنیتی متنی را میبیند که با پیشوند مجاز شروع شده و اجازه عملیات را میدهد؛ اما در واقعیت، فایل در پوشه ممنوعه ذخیره میشود.

سازوکار شکست
به نقل از گزارش dev.to، این شکست به این دلیل رخ میدهد که توابع path.resolve و path.relative صرفاً عملیات متنی هستند. آنها نقاط و جداکنندهها را اصلاح میکنند اما هرگز برای دیدن مقصد واقعی، سیستمفایل را باز نمیکنند. Resolve روی یک مسیر، در واقع ادعایی دربارهی متن است، نه حقیقتی دربارهی سیستمفایل. گارد امنیتی دو «نام» را با هم مقایسه میکند، در حالی که هدف باید محدود کردن دو «مکان فیزیکی» باشد.
این یک باگ کلاسیک پیمایش نیست که مهاجم عبارت .. را وارد کند. در اینجا ورودی هیچ مورد مشکوکی ندارد؛ نه .. وجود دارد، نه مسیر مطلق و نه ترفندهای کدگذاری. تغییر مسیر روی دیسک اتفاق افتاده، نه در درخواست. این موضوع باعث میشود ریسک مورد نظر به راحتی نادیده گرفته شود، در حالی که Symlinkها در محیطهای توسعه مدرن بسیار رایج هستند:
- پوشه node_modules بهطور پیشفرض و بر اساس طراحی، پر از پیوندهای نمادین است.
- مدیریت بسته در pnpm از ساختاری متکی بر Symlink برای مدیریت پکیجها استفاده میکند.
- ابزارهای ساخت (Build tools) بهطور مکرر کشها را در جای خود لینک میکنند.
- ابزارهای Monorepo بستهها را به یکدیگر متصل میکنند.
- توسعهدهندگان اغلب پوشه
data/را به یک دیسک فیزیکی بزرگتر با استفاده از لینکها متصل میکنند.
علاوه بر این، خودِ عامل میتواند منبع ایجاد لینک باشد. عاملی که بدون نظارت دستورات شل (Shell) را اجرا میکند، میتواند از طریق ln -s یک لینک دایرکتوری بسازد. چون Hookها معمولاً نام ابزارها را تطبیق میدهند و Bash بهطور معمول بهعنوان یک ابزار «نوشتن فایل» دستهبندی نمیشود، ایجاد لینک به عنوان یک رویداد Write شناسایی نمیشود. یک دستور ساده مسیر فرار را میسازد و هر نوشته بعدی از طریق آن، توسط گاردی که دقیقاً طبق دستورالعمل متنیاش عمل میکند، تأیید میشود.
Junctionهای ویندوز و توهم دسترسی
بسیاری از توسعهدهندگان تصور میکنند این مشکل مختص یونیکس است، زیرا ایجاد Symlink در ویندوز معمولاً نیاز به دسترسی Administrator یا فعال بودن Developer Mode دارد. اما ویندوز قابلیتی به نام Directory Junctions دارد که این محدودیت را دور میزند.
تستهای انجام شده روی ویندوز ۱۰ (در حالی که Developer Mode غیرفعال بود) نتایج زیر را نشان داد:
- شکست در ایجاد: EPERM dir link (symlink)
- شکست در ایجاد: EPERM file link (symlink)
- موفقیت: dir link (junction)
یک Junction دایرکتوری بدون هیچ هشدار یا نیاز به ارتقای دسترسی توسط یک پردازش کاربر عادی ایجاد شد. تنها یک خط کد در Node.js مانند fs.symlinkSync(target, path, "junction") یا یک خط دستور در شل مانند mklink /J برای ایجاد مسیر فرار کافی است. در واقع، دروازه امنیتی که همه به آن تکیه کرده بودند، دری را guarding میکرد که از قبل باز بود.
شکست گاردهای دو لایه
برخی تیمها برای جبران محدودیتهای Hook، لایه دومی را با اجرای git status --porcelain قبل از Commit اضافه میکنند. آنها مسیرهای تغییر یافته را با همان لیست مجاز (Allow-list) چک میکنند تا هرگونه خروج از مسیر را شناسایی کنند.
اما این لایه دوم دقیقاً مشابه لایه اول شکست میخورد. در بازسازی این حمله، دستور git status --porcelain مسیر را به صورت products/cache/ گزارش کرد. بررسی محدوده (Scope check) نتیجه را به این صورت برگرداند: {"ok":true,"changed":["products/cache/"],"violations":[]}. چون هر دو بررسی از یک منطق ناممحور استفاده میکنند، نقطه کور یکسانی دارند. دو بررسی مستقل که به یک شکل شکست میخورند، در واقع فقط یک بررسی با دو لباس مختلف هستند.
پیادهسازی راهکار اصلاحی
برای بستن این حفره، توسعهدهندگان باید از سیستمفایل بپرسند که مسیر واقعاً به کجا میرود. این کار با استفاده از realpathSync امکانپذیر است. راهکار پیشنهادی شامل یک حلقه است که تا نزدیکترین جد موجود بالا میرود، مسیر واقعی را پیدا میکند و سپس بخشهای موجود نیستهی فایل هدف را دوباره به آن میچسباند. این حلقه ضروری است چون هدفِ یک عملیات Write معمولاً هنوز وجود ندارد و فراخوانی realpath روی مسیری که وجود ندارد، خطای ENOENT میدهد.
import { realpathSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
export function resolveLinks(path, realpath = realpathSync) {
let current = resolve(path);
const tail = [];
for (;;) {
try {
return tail.length ? join(realpath(current), ...tail) : realpath(current);
} catch (err) {
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") return null;
const parent = dirname(current);
if (parent === current) return resolve(path);
tail.unshift(basename(current));
current = parent;
}
}
}
نکته حیاتی این است که هم مسیر هدف و هم ریشه مجاز (Allow-list root) باید Resolve شوند. اگر فقط هدف بررسی شود، ممکن است دسترسیهای قانونی رد شوند؛ زیرا در بسیاری از موارد، ریشه پروژه خودش از طریق یک لینک در دسترس است. این موضوع در سناریوهای زیر رایج است:
- در macOS، مسیر
/tmpدر واقع/private/tmpاست. - مسیر
/homeاغلب یک نقطه اتصال (Mount point) پشت یک لینک است. - مسیرهای Checkout در CI مکرراً Symlinkهایی به یک دایرکتوری Workspace هستند.
Resolve کردن تنها یک طرف، بدتر از Resolve نکردن هر دو طرف است، زیرا باعث میشود گارد در موارد عادی شکست بخورد، نه در موارد نادر.
سیاستهای اصلاحشده برای مدیریت خطا
در هنگام پیادهسازی این Resolution، دو تصمیم آگاهانه باید در مورد شکست و گزارشدهی گرفته شود:
۱. رد در صورت شکست در Resolution: در حالی که اکثر شکستهای Hook باید برای جلوگیری از توقف کار به دلیل غلطهای تایپی «ادامه یابند»، این مورد یک استثنا است. اگر realpath خطای ELOOP (حلقه Symlink) یا EACCES (دایرکتوری غیرقابل خواندن) برگرداند، گارد باید عملیات نوشتن را رد کند. مکانی که ناشناخته است، مکان امنی نیست.
۲. پیامهای خطای دقیق: مسیر Resolve شده باید در پیام خطا ذکر شود. برای مثال: "Write to .../project/products/cache/notes.txt is outside this agent's write scope. It resolves to .../secrets/notes.txt. Allowed: products."
شکاف باقیمانده
حتی با realpathSync نیز شکاف زمانی بین بررسی و استفاده (TOCTOU - Time-of-check-to-time-of-use) باقی میماند. بررسی در Hook اتفاق میافتد، اما نوشتن پس از بازگشت Hook رخ میدهد. تئوریکاً میتوان در این پنجره میلیثانیهای، لینک را ایجاد یا تغییر داد. هیچ چیزی در یک PreToolUse hook نمیتواند این پنجره را ببندد.
این موضوع تأیید میکند که یک Software Hook تنها گاردی در برابر اشتباهات یا دستورات مخرب خوانده شده از فایلها است، نه یک Sandbox واقعی. یک مرز امنیتی واقعی نیازمند ایزولاسیون در سطح سیستمعامل مانند کانتینرها، یک حساب کاربری اختصاصی با دید محدود به دیسک، یا یک Filesystem Namespace است.
تست برای شناسایی حفره
دلیل اینکه این باگ اغلب از بررسیها جان سالم به در میبرد این است که مجموعههای تست معمولاً فقط تأیید میکنند که گارد موارد درست را اجازه میدهد و پیمایشهای آشکار را رد میکند. برای تست صحیح این مورد، توسعهدهندگان باید:
- یک لینک واقعی در یک دایرکتوری موقت ایجاد کنند.
- یک عملیات Write را از طریق آن لینک هدف قرار دهند.
- رد شدن عملیات (Denial) را تأیید کنند.
این کار باید روی یک سیستمفایل واقعی — با استفاده از Junctionها در ویندوز و Symlinkها در سایر سیستمها — انجام شود، نه با استفاده از یک realpath شبیهسازی شده (Mocked). بررسیای که هرگز شکستش دیده نشده، دلیلی بر امنیت نیست.
گام بعدی شما
- اگر از عاملهای هوش مصنوعی برای مدیریت فایل استفاده میکنید، توابع
path.resolveرا باrealpathSyncجایگزین کنید. - در محیطهای ویندوزی، حتماً اثر Directory Junctionها را در تستهای امنیتی خود بررسی کنید.
- برای امنیت حداکثری، عاملها را در کانتینرهای ایزوله با دسترسی محدود به دیسک (Read-only root) اجرا کنید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو