رفتن به محتوای اصلی

سیاست همان‌مبدأ و پروکسی‌های وب: تحلیل امنیتی فنی

نویسنده
Smith
Smith
زمان مطالعه
2 دقیقه مطالعه
تاریخ انتشار
۱۱ اسفند ۱۴۰۴
سیاست همان‌مبدأ و پروکسی‌های وب: تحلیل امنیتی فنی

وقتی داشتیم ProxyOrb را می‌ساختیم، در هر تصمیم طراحی با یک پارادوکس اساسی روبرو می‌شدیم: یک پروکسی وب با وادار کردن مرورگر به این باور که محتوای third-party همان‌مبدأ است کار می‌کند. این به خودی خود یعنی فریب دادن مدل امنیتی اصلی مرورگر. در عین حال، مرورگرهای مدرن طی پانزده سال گذشته لایه‌های دفاعی پیچیده‌تری را دقیقاً در برابر همین نوع سردرگمی مبدأ اضافه کرده‌اند.

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

موضوعاتی که پوشش می‌دهیم: پایه SOP، بازنویسی URL، CORS، CORB/CORP، COEP/COOP، Service Worker، iframe و پیامدهای امنیتی آن برای هم اپراتورهای پروکسی وب و هم کاربران.


۱. پایه سیاست همان‌مبدأ

سیاست همان‌مبدأ (SOP) بر اساس یک سه‌تایی تعریف می‌شود: scheme + host + port. دو URL فقط زمانی همان‌مبدأ هستند که هر سه مؤلفه دقیقاً یکسان باشند. https://example.com:443 و http://example.com:80 علی‌رغم اینکه به یک سرور اشاره دارند، cross-origin هستند.

چیزی که اغلب به اشتباه فهمیده می‌شود اینست که SOP دقیقاً چه چیزی را جلوگیری می‌کند در مقابل آنچه اجازه می‌دهد. SOP این موارد را بلوک نمی‌کند:

  • لود کردن تصاویر (<img src>) از مبداهای دیگر
  • لود کردن اسکریپت‌ها (<script src>) از مبداهای دیگر
  • لود کردن استایل‌شیت‌ها (<link rel="stylesheet">) از مبداهای دیگر
  • جاسازی iframe از مبداهای دیگر (هرچند محتوای داکیومنت جاسازی‌شده ایزوله می‌شود)
  • ارسال فرم (<form action>) به مبداهای دیگر

آنچه SOP جلوگیری می‌کند خواندن پاسخ درخواست‌های cross-origin ارسال‌شده از طریق fetch() یا XMLHttpRequest است. درخواست رد می‌شود — رفت‌وبرگشت شبکه انجام می‌شود — اما بدنه و هدرهای پاسخ از JavaScript اجراشونده در یک مبدأ متفاوت پنهان می‌مانند.

این تمایز برای پروکسی‌های وب بسیار اهمیت دارد. کل هدف پروکسی این است که دو مبدأ (سرور پروکسی و سرور هدف) را در یکی ادغام کند و مرز cross-origin را حذف نماید. وقتی همه منابع به نظر می‌رسد از https://proxyorb.com می‌آیند، محدودیت‌های SOP در خواندن پاسخ‌ها دیگر اعمال نمی‌شود.

«فضای مشروع» که پروکسی‌های وب از آن بهره می‌برند بازنویسی URL است: به جای https://example.com/api/data، هر درخواست به https://proxyorb.com/api/data?__pot=aHR0cHM6Ly9leGFtcGxlLmNvbQ== تبدیل می‌شود. از دیدگاه مرورگر، این یک درخواست همان‌مبدأ است. SOP دور زده نشده — بلکه با تغییر مبدأ ظاهری همه محتوا، بی‌ربط شده است.


۲. معماری بازنویسی URL در ProxyOrb

برای درک پیامدهای امنیتی، باید مکانیزم رمزگذاری را بفهمید. ProxyOrb از یک پارامتر URL به نام __pot (proxy origin token) استفاده می‌کند که مبدأ هدف را به صورت Base64 رمزگذاری‌شده نگه می‌دارد.

پارامتر __pot

پارامتر __pot همیشه مبدأ (scheme + host، بدون path) هدف را رمزگذاری می‌کند، نه URL کامل. این یعنی https://example.com/some/deep/path?foo=bar و https://example.com/other/page هر دو مقدار یکسانی برای __pot تولید می‌کنند: aHR0cHM6Ly9leGFtcGxlLmNvbQ== (رمزگذاری Base64 از https://example.com). path و query string واقعی به همان شکل در path و query string خود URL پروکسی حفظ می‌شوند.

وقتی کاربر به https://proxyorb.com/?__pot=aHR0cHM6Ly9leGFtcGxlLmNvbQ== می‌رود، gateway آن را رمزگشایی کرده و URL اصلی را بازسازی می‌کند:

-- Gateway pseudocode: decoding the proxy request

function resolve_original_url(proxy_url):
    pot_value = parse_query_param(proxy_url, "__pot")
    if not pot_value:
        return error("Missing origin token")

    original_origin = base64_decode(pot_value)
    -- e.g. "https://example.com"

    validate_not_private_ip(original_origin)  -- SSRF protection

    -- Replace the proxy host with the target host, keep path/query intact
    original_url = replace_origin(proxy_url, original_origin)
    return original_url

سپس gateway درخواست را به سرور هدف واقعی فوروارد می‌کند و هدر Origin را بازنویسی می‌کند تا سرور upstream دامنه خودش را ببیند نه دامنه پروکسی:

-- Set outbound headers toward the target server

request.headers["Host"]   = target_host
request.headers["Origin"] = target_origin   -- e.g. "https://example.com"
-- Remove all internal proxy headers before forwarding

بازنویسی URL سمت کلاینت از طریق Service Worker

gateway سمت سرور را مدیریت می‌کند. سمت کلاینت — بازنویسی URL‌ها در پاسخ‌های HTML، JavaScript و CSS تا همه درخواست‌های فرعی از طریق پروکسی ادامه پیدا کنند — توسط یک Service Worker مدیریت می‌شود.

تبدیل اصلی هر URL موجود در محتوای صفحه را به فرمت پروکسی تبدیل می‌کند:

-- Service Worker pseudocode: toProxyURL()

function toProxyURL(originalUrl, currentPageUrl):
    if isSameOrigin(originalUrl, currentPageUrl):
        return originalUrl   -- already proxy-origin, no rewrite needed

    -- Extract path/query/hash from the original URL
    -- Build a new URL rooted at the proxy origin
    proxyPath   = extractPathAndQuery(originalUrl)
    potValue    = base64encode(extractOrigin(originalUrl))
    proxyUrl    = proxy_origin + proxyPath + "?__pot=" + potValue

    return proxyUrl

برای مثال، https://cdn.example.com/bundle.js که در صفحه‌ای در حال پروکسی شدن ارجاع داده شده، به https://proxyorb.com/bundle.js?__pot=aHR0cHM6Ly9jZG4uZXhhbXBsZS5jb20= تبدیل می‌شود.

این بازنویسی جامع است: fetch()، XMLHttpRequest، <script src>، <img src>، اتصالات WebSocket و تگ‌های <link> همه را پوشش می‌دهد. وقتی هر URL در صفحه به مبدأ پروکسی اشاره می‌کند، مرورگر هرگز درخواست cross-origin ارسال نمی‌کند — هر درخواست از ساختار ذاتی همان‌مبدأ است.

بازیابی __pot بدون ناوبری کامل

یک edge case ظریف با درخواست‌هایی پیش می‌آید که از داخل یک صفحه پروکسی‌شده سرچشمه می‌گیرند اما پارامتر __pot ندارند — مثلاً یک fetch('/api/data') نسبی که قبل از اینکه Service Worker URL را بازنویسی کند فعال می‌شود، یا درخواستی که یک اسکریپت تزریق‌شده پویا منتشر کرده و از بازنویسی URL فرار کرده.

Service Worker توکن گمشده را از طریق یک جستجوی آبشاری حل می‌کند:

-- Service Worker pseudocode: recovering the origin token

function resolveMissingPot(fetchEvent):
    candidates = [
        getUrlOfControlledTab(fetchEvent.clientId),   -- most reliable
        inferUrlFromFetchEvent(fetchEvent),             -- from clientId / resultingClientId
        fetchEvent.request.referrer,                    -- referrer header
    ]

    for url in candidates:
        pot = extractQueryParam(url, "__pot")
        if pot: return pot

    return null   -- cannot recover; let the gateway handle or reject

gateway هم یک مسیر بازیابی موازی دارد: اگر درخواستی بدون __pot برسد اما هدر Referer حاوی یک URL همان‌مبدأ باشد که __pot را در خود دارد، gateway توکن را از referer استخراج کرده و به درخواست جاری اضافه می‌کند. این سناریوهای ناوبری را که توکن توسط JavaScript خود صفحه قبل از رسیدن به gateway حذف شده مدیریت می‌کند.


۳. CORS: سیستم مجوز صریح cross-origin

CORS (Cross-Origin Resource Sharing) طراحی شده تا به سرورها اجازه دهد با ارسال هدرهای پاسخ خاص، داوطلبانه دسترسی cross-origin را فعال کنند. از دیدگاه پروکسی، CORS دو مشکل متمایز ایجاد می‌کند.

مشکل اول: رهگیری Preflight CORS

وقتی JavaScript روی یک صفحه یک درخواست fetch() با هدرهای غیرساده ارسال می‌کند (مثلاً Authorization، Content-Type: application/json)، مرورگر ابتدا یک درخواست preflight از نوع OPTIONS ارسال می‌کند. اگر پاسخ preflight سرور هدف شامل هدرهای CORS مجاز نباشد، درخواست واقعی بلوک می‌شود.

در معماری ProxyOrb، Service Worker تمام درخواست‌های OPTIONS را رهگیری کرده و به صورت مصنوعی — قبل از اینکه حتی به gateway برسند — پاسخ می‌دهد و یک پاسخ برمی‌گرداند که درخواست واقعی را آزاد می‌کند:

-- Service Worker pseudocode: synthetic CORS preflight

function handlePreflight(request):
    origin = request.headers["Origin"] or "*"

    return Response(status=204, headers={
        "Access-Control-Allow-Origin":      origin,
        "Access-Control-Allow-Methods":     "GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD",
        "Access-Control-Allow-Headers":     "*",
        "Access-Control-Allow-Credentials": "true",
        "Access-Control-Max-Age":           "86400",
        "Vary":                             "Origin",
    })

در سطح gateway، هر پاسخ پروکسی‌شده هدرهای CORS معادل دریافت می‌کند:

-- Gateway pseudocode: set CORS on outbound response

response.headers["Access-Control-Allow-Origin"]      = original_origin
response.headers["Access-Control-Allow-Headers"]     = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"

مشکل دوم: درخواست‌های Credentialed

ترکیب Access-Control-Allow-Credentials: true و یک Access-Control-Allow-Origin غیر wildcard اهمیت دارد. CORS وقتی credential‌ها درج می‌شوند، یک مقدار مبدأ صریح (نه *) می‌خواهد. Service Worker همه درخواست‌های خروجی را با credentials: 'include' ارسال می‌کند، بنابراین gateway باید دقیقاً مبدأ درخواست‌کننده را برگرداند نه یک wildcard.

این یعنی همه کوکی‌های ذخیره‌شده زیر proxyorb.com — که از هر سایت هدفی که کاربر از طریق پروکسی بازدید کرده جمع شده‌اند — با هر درخواست پروکسی‌شده ارسال می‌شوند. این عمدی است (وضعیت session برای سایت‌هایی که کاربر وارد شده را حفظ می‌کند)، اما یک ملاحظه امنیتی مهم است که در بخش ۸ بحث می‌کنیم.

الگوی Strip-and-Replace هدرهای CORS در سرور پروکسی

یک پاسخ پروکسی‌شده ممکن است هدرهای CORS سرور هدف را داشته باشد که از دیدگاه مرورگر غلط هستند (آنها به مبدأ هدف اشاره می‌کنند نه مبدأ پروکسی). Service Worker آنها را حذف و جایگزین می‌کند:

-- Service Worker pseudocode: sanitize response headers

function sanitizeResponseHeaders(response):
    headers = copy(response.headers)

    -- Remove target-site directives that would confuse the browser
    headers.delete("Content-Security-Policy")
    headers.delete("Content-Security-Policy-Report-Only")
    headers.delete("X-Frame-Options")
    headers.delete("X-Content-Type-Options")

    -- Replace with proxy-origin-correct CORS headers
    headers.set("Access-Control-Allow-Origin",      proxy_origin)
    headers.set("Access-Control-Allow-Credentials", "true")
    headers.set("Access-Control-Allow-Headers",     "*")
    headers.set("Vary", "Origin")

    return headers

۴. CORB و CORP: دفاع‌های سخت‌گیرانه‌تر Chromium

CORS بر opt-in صریح از سمت سرورها متکی است. Chromium دو مکانیزم اضافه کرده که به صورت پیش‌فرض فعال هستند، صرف‌نظر از تنظیمات CORS.

Cross-Origin Read Blocking (CORB)

CORB در Chrome 67 (2018) به عنوان بخشی از اقدامات مقابله با Spectre معرفی شد. ایده اصلی این است: حتی اگر بدنه پاسخ cross-origin هرگز توسط JavaScript خوانده نشود، این واقعیت که توسط پروسه renderer واکشی و decode شده یعنی در فضای آدرس آن پروسه وجود دارد. حملات از نوع Spectre می‌توانند آن را از طریق کانال‌های جانبی استخراج کنند.

CORB از ورود برخی پاسخ‌های cross-origin به پروسه renderer جلوگیری می‌کند. مشخصاً، وقتی یک تگ <script> یا <img> یک پاسخ cross-origin واکشی می‌کند و آن پاسخ Content-Type از نوع text/html، text/xml یا application/json دارد، Chromium بدنه را بررسی می‌کند (با استفاده از یک MIME-type sniffer تعریف‌شده در مشخصات CORB) و اگر نوع مطابقت داشت، پاسخ را قبل از اینکه renderer آن را ببیند با یک پاسخ خالی جایگزین می‌کند.

برای یک پروکسی وب، CORB اگر درخواست‌ها واقعاً cross-origin بودند فاجعه‌بار می‌شد. یک endpoint API که application/json برمی‌گرداند و از یک تگ <script> واکشی می‌شود، به طور خاموش خالی برمی‌گشت. اما از آنجایی که ProxyOrb همه URL‌ها را همان‌مبدأ بازنویسی می‌کند، شرط cross-origin CORB هرگز فعال نمی‌شود — مرورگر هرگز یک پاسخ JSON cross-origin نمی‌بیند، چون از دیدگاه آن همه پاسخ‌ها از proxyorb.com می‌آیند.

یک حالت که CORB اهمیت دارد در دوره انتقالی است، قبل از اینکه Service Worker کاملاً ثبت شده و درخواست‌ها را رهگیری کند. اگر در آن پنجره درخواستی از بازنویسی URL فرار کند، CORB ممکن است آن را بلوک کند. این race condition دلیلی است که ProxyOrb یک لایه رهگیر main-thread هم دارد که XMLHttpRequest، fetch و setter‌های attribute DOM را به صورت synchronous قبل از اجرای هر JavaScript صفحه patch می‌کند — و یک fallback در طول راه‌اندازی Service Worker فراهم می‌کند.

شایان ذکر است که CORB تا حدی توسط Opaque Response Blocking (ORB) جایگزین شده که Chromium در حال پذیرش آن است. ORB قوانین CORB را برای کاهش false positive‌ها در حین حفظ محافظت‌های Spectre اصلاح می‌کند. پیامدهای سازگاری با پروکسی مشابه هستند.

Cross-Origin Resource Policy (CORP)

CORP، تعریف‌شده در مشخصات Fetch، به سرورها اجازه می‌دهد اعلام کنند که منابع آنها فقط باید توسط contexthای همان‌مبدأ یا همان‌سایت لود شوند:

Cross-Origin-Resource-Policy: same-origin

وقتی Chromium این هدر را روی یک درخواست sub-resource از نوع cross-origin مشاهده می‌کند، پاسخ را کاملاً بلوک می‌کند. این از CORB تهاجمی‌تر است — صرف‌نظر از نوع محتوا اعمال می‌شود و نمی‌توان با MIME sniffing آن را دور زد.

دوباره، از آنجایی که بازنویسی URL در ProxyOrb اطمینان می‌دهد همه درخواست‌ها از دیدگاه مرورگر همان‌مبدأ هستند، هدرهای CORP سرورهای هدف بلوک را فعال نمی‌کنند. gateway هم این هدرها را از پاسخ‌ها به صورت دفاعی حذف می‌کند تا edge caseهایی که درخواستی بدون بازنویسی URL رد می‌شود را مدیریت کند.

مشکل عمیق‌تر CORP در واقع یک حالت است که معماری پروکسی به سازگاری کمک می‌کند: اگر https://api.example.com و https://www.example.com هر دو پروکسی می‌شوند، هر دو به همان مبدأ پروکسی map می‌شوند، بنابراین یک fetch از یکی به دیگری اکنون واقعاً همان‌مبدأ است — محدودیت‌های CORP دیگر بین آنها اعمال نمی‌شود.


۵. COEP و COOP: جداسازی cross-origin

از Chrome 92 (2021)، دسترسی به SharedArrayBuffer — و به تبع آن، تایمرهای با دقت بالا که برای برخی API‌های performance استفاده می‌شوند — به صفحاتی محدود شد که از جداسازی cross-origin استفاده می‌کنند. این opt-in نیاز به دو هدر پاسخ دارد که با هم کار کنند.

Cross-Origin-Embedder-Policy (COEP)

Cross-Origin-Embedder-Policy: require-corp

وقتی یک صفحه این هدر را ارسال می‌کند، مرورگر اجرا می‌کند که هر sub-resource لود شده توسط آن صفحه یا:

  1. همان‌مبدأ است، یا
  2. صراحتاً Cross-Origin-Resource-Policy: cross-origin ارسال می‌کند، یا
  3. یک پاسخ CORS مجاز برای fetch با credential دارد

مشکل برای پروکسی‌های وب: اگر یک سایت هدف COEP: require-corp روی داکیومنت اصلی‌اش ارسال کند و آن داکیومنت از طریق پروکسی با هدر حفظ‌شده سرو شود، مرورگر اکنون مطالبه می‌کند که هر sub-resource لود شده توسط آن صفحه هم opt-in کند. هر منبعی که این کار را نکند — و اکثر نمی‌کنند — باعث خطای شبکه می‌شود.

Cross-Origin-Opener-Policy (COOP)

Cross-Origin-Opener-Policy: same-origin

COOP کنترل می‌کند که آیا یک صفحه می‌تواند یک browsing context group را با صفحات cross-origin به اشتراک بگذارد. وقتی روی same-origin تنظیم شود، یک ناوبری cross-origin یک browsing context group جدید باز می‌کند که ارجاعات window.opener و کانال‌های postMessage بین صفحه و هر opener از نوع cross-origin را می‌شکند.

برای یک پروکسی، COOP نسبت به COEP بلافاصله مخرب‌تر نیست، اما همچنان سایت‌هایی را که به جریان‌های cross-origin postMessage متکی هستند (جریان‌های popup OAuth که رایج‌ترین مثال هستند) می‌شکند.

معضل اپراتور پروکسی

این سخت‌ترین trade-off است که در ProxyOrb با آن روبرو هستیم:

گزینه الف: حذف COEP و COOP از پاسخ‌ها.

  • مزیت: لود sub-resource به طور معمول کار می‌کند؛ صفحه پروکسی‌شده این محدودیت‌ها را اعمال نمی‌کند.
  • معایب: هدرهای امنیتی که سایت هدف عمداً تنظیم کرده را حذف می‌کنیم. اگر سایت از جداسازی cross-origin برای محافظت از داده حساس در برابر حملات Spectre استفاده می‌کرد، حذف این هدرها آن ریسک را دوباره فعال می‌کند.

گزینه ب: حفظ COEP و COOP.

  • مزیت: قصد امنیتی سایت هدف محترم شمرده می‌شود.
  • معایب: هر sub-resourceای که CORP: cross-origin را صراحتاً تنظیم نکرده لود نمی‌شود، که اکثر برنامه‌های وب پیچیده را می‌شکند.

استراتژی فعلی ProxyOrb گزینه الف است: gateway Cross-Origin-Embedder-Policy و Cross-Origin-Opener-Policy را از پاسخ‌ها حذف می‌کند. این سازگاری سایت را به حداکثر می‌رساند. trade-off امنیتی آگاهانه انجام می‌شود: کاربران یک پروکسی وب می‌پذیرند که محتوا را در محیطی متفاوت از زمینه استقرار مورد نظرش اجرا می‌کنند.

هر مکانیزم امنیتی جدید مرورگر یک الگو دارد: به عنوان opt-in معرفی می‌شود (سایت‌ها می‌توانند اگر می‌خواهند محافظت باشند هدر را ارسال کنند)، سپس به تدریج برای API‌های جدید پیش‌فرض می‌شود و در نهایت اجرای اجباری آن در نظر گرفته می‌شود. COEP این مسیر را طی کرد: اختیاری در Chrome 83، لازم برای SharedArrayBuffer در Chrome 91. سایت‌هایی که محتوای third-party را بدون هماهنگی جاسازی می‌کردند محتوایشان را شکسته یافتند. پروکسی‌های وب این مشکل را تشدید می‌کنند چون به تعریف محتوای third-party را واسطه می‌کنند.

جامعه امنیت مرورگر از این مشکل آگاه است. پیشنهاد در حال ظهور Document-Isolation-Policy هدفش جداسازی ایزوله‌سازی پروسه از نیازمندی‌های جداسازی cross-origin است، که بالقوه امکان دریافت اقدامات مقابله Spectre را بدون نیاز به opt-in همه sub-resourceها می‌دهد. وقتی آن منتشر شود، سازگاری پروکسی با هدرهای جداسازی ممکن است بهبود یابد.


۶. Service Worker به عنوان لایه اعتماد سمت مرورگر

Service Worker صرفاً یک بهینه‌سازی نیست — از نظر معماری برای مدل امنیتی ProxyOrb ضروری است. اینجاست که توضیح می‌دهیم چرا.

اسکوپ ثبت به مبدأ گره خورده

یک Service Worker با یک scope ثبت می‌شود که همیشه در مبدأ صفحه ثبت‌کننده است. وقتی ProxyOrb Service Worker خود را ثبت می‌کند، scope https://proxyorb.com/ است، یعنی هر fetch از هر صفحه‌ای در آن مبدأ را رهگیری می‌کند.

این دلیل اساسی است که بازنویسی URL به همان‌مبدأ کار می‌کند: وقتی SW یک client را کنترل می‌کند، هر درخواست شبکه از آن client — صرف‌نظر از اینکه JavaScript در صفحه چه URL‌ای را fetch می‌کند — ابتدا از Service Worker رد می‌شود.

// Service Worker entry point
self.addEventListener('fetch', (event) => {
  event.respondWith(handleProxyRequest(event))
})

Pipeline رهگیری

وقتی Service Worker یک درخواست را رهگیری می‌کند، از چندین مرحله تصمیم رد می‌شود:

-- Service Worker pseudocode: request interception pipeline

function handleProxyRequest(fetchEvent):
    request = fetchEvent.request

    -- Stage 1: Synthetic CORS preflight (never hits the network)
    if request.method == "OPTIONS":
        return syntheticCORSResponse(request)

    -- Stage 2: Pass through requests that shouldn't be proxied
    if shouldBypass(request.url):
        return originalFetch(request)

    -- Stage 3: Ensure __pot is present; recover from context if missing
    enrichedRequest = attachOriginToken(request, fetchEvent)

    -- Stage 4: Forward to gateway
    response = originalFetch(enrichedRequest)

    -- Stage 5: Strip and replace security headers in the response
    return sanitizeAndReturn(response)

فوروارد شفاف هدر

یک چالش ظریف: مرورگر از تنظیم برخی هدرهای درخواست «ممنوع» (Host، Origin، Referer و غیره) از طریق Fetch API توسط JavaScript جلوگیری می‌کند. Service Worker این محدودیت را با رمزگذاری هدرهای محدودشده در یک هدر passthrough سفارشی دور می‌زند؛ gateway سپس آنها را قبل از فوروارد به سرور هدف decode و اعمال می‌کند:

-- Pseudocode: encode browser-restricted headers for the gateway

function addPassthroughHeaders(request, outboundHeaders):
    restricted = {}
    for name, value in request.headers:
        if name not in COMMON_ALLOWED_HEADERS:
            restricted[name] = value

    if restricted is not empty:
        outboundHeaders["X-Proxy-Passthrough"] = json_encode(restricted)

-- Gateway side: decode and apply before forwarding upstream
function applyPassthroughHeaders(incomingHeaders):
    encoded = incomingHeaders["X-Proxy-Passthrough"]
    if encoded:
        for name, value in json_decode(encoded):
            request.headers[name] = value
        request.headers.delete("X-Proxy-Passthrough")

پیامدهای امنیتی SW به عنوان واسط مورد اعتماد

از دیدگاه امنیتی، Service Worker یک موقعیت ممتاز اشغال می‌کند: می‌تواند هر درخواستی از هر صفحه‌ای در اسکوپش را بررسی، اصلاح و جعل کند. برای استفاده مشروع از پروکسی، این دقیقاً همان هدف است. برای یک پروکسی مخرب، این یک سطح حمله بسیار قدرتمند خواهد بود.

به همین دلیل قابل اعتماد بودن خود اسکریپت Service Worker بسیار مهم است. ProxyOrb اسکریپت interceptor را از مبدأ خودش از طریق HTTPS با کنترل‌های cache سختگیرانه ارائه می‌دهد. اگر یک مهاجم بتواند یک Service Worker اصلاح‌شده تزریق کند، می‌تواند همه ترافیک کاربران آسیب‌دیده را رهگیری کند — نه فقط ترافیک پروکسی، بلکه هر درخواستی از صفحات زیر آن مبدأ.


۷. iframe: مشکل Frame-Ancestors

iframeها یک چالش متمایز از sub-resourceهای معمولی نشان می‌دهند چون شامل یک browsing context تودرتو با ناوبری خودش، مرز SOP خودش و مجموعه محدودیت‌های جاسازی خودش هستند.

X-Frame-Options و frame-ancestors

دو مکانیزم از جاسازی صفحات در iframe جلوگیری می‌کنند:

قدیمی: X-Frame-Options: DENY یا X-Frame-Options: SAMEORIGIN

مدرن: Content-Security-Policy: frame-ancestors 'none' یا frame-ancestors 'self'

وقتی gateway یک صفحه هدف را پروکسی می‌کند که هر یک از این هدرها را ارسال می‌کند، صفحه نمی‌تواند در یک iframe زیر مبدأ پروکسی جاسازی شود. از آنجایی که X-Frame-Options: SAMEORIGIN به مبدأ هدف اشاره می‌کند (example.com) و پروکسی صفحه را از proxyorb.com ارائه می‌دهد، بررسی همان‌مبدأ مرورگر شکست می‌خورد.

پروکسی باید این هدرها را از پاسخ‌های پروکسی‌شده حذف کند و CSP را با یک نسخه مجاز جایگزین کند:

-- Gateway pseudocode: override embedding-restrictive headers

response.headers.delete("X-Frame-Options")
response.headers.delete("Content-Security-Policy")
response.headers.delete("Content-Security-Policy-Report-Only")

-- Set a permissive replacement that allows the proxy to embed content
response.headers["Content-Security-Policy"] =
    "upgrade-insecure-requests; frame-ancestors 'self'; " +
    "default-src * data: blob: about: ws: wss: 'unsafe-inline' 'unsafe-eval'"

رهگیری iframe و تزریق اسکریپت

iframeهای جاسازی‌شده یک مشکل دوم ایجاد می‌کنند: محتوای آنها از پروکسی لود می‌شود، اما browsing context iframe به طور خودکار Service Worker یا رهگیر main-thread را فعال ندارد. اگر صفحه پروکسی‌شده یک <iframe src="https://widget.example.com/..."> ایجاد کند، آن URL iframe هم باید به فرمت پروکسی بازنویسی شود، و اسکریپت‌های interceptor پروکسی باید به داکیومنت iframe تزریق شوند.

رهگیر iframe در دو سطح کار می‌کند:

سطح ۱ — Prototype patching (ایجاد برنامه‌نویسی iframe را می‌گیرد):

-- Pseudocode: intercept iframe src assignment

override HTMLIFrameElement.prototype.src setter:
    if value is not already a proxy URL:
        value = toProxyURL(value)   -- rewrite to proxy format
    call original setter(value)
    scheduleProxyInjection(this)    -- inject interceptor into iframe document

سطح ۲ — MutationObserver fallback (iframeهای درج‌شده در DOM را می‌گیرد):

-- Pseudocode: observe DOM for dynamically added iframes

observer = new MutationObserver(mutations):
    for mutation in mutations:
        for node in mutation.addedNodes:
            if node is an <iframe>:
                rewriteSrcIfNeeded(node)
                injectProxyScript(node)

observer.observe(document, { childList: true, subtree: true })

مشکل ویژگی sandbox

ویژگی HTML sandbox محدود می‌کند که یک iframe چه کاری می‌تواند انجام دهد: sandbox="allow-scripts allow-same-origin" اجرای اسکریپت، ارسال فرم، دسترسی cross-origin و غیره را کنترل می‌کند. برای اینکه تزریق پروکسی کار کند، iframe باید اسکریپت‌ها را اجرا کرده و به context پروکسی parent دسترسی داشته باشد.

توکن allow-same-origin به خصوص پیچیده است: وقتی به همراه allow-scripts وجود دارد، ایزوله‌سازی مبدأ sandbox را شکست می‌دهد — iframe با مبدأ صفحه جاسازی‌کننده اجرا می‌شود. وقتی غایب است، iframe یک مبدأ opaque منحصربه‌فرد می‌گیرد که تزریق اسکریپت پروکسی را کاملاً می‌شکند.

رویکرد فعلی ProxyOrb برای ویژگی‌های sandbox این است که ویژگی sandbox را کاملاً قبل از تزریق اسکریپت پروکسی حذف کند:

-- Pseudocode: handle sandbox attribute before proxy injection

function handleSandboxedIframe(iframe):
    if iframe.hasAttribute("sandbox"):
        -- Remove so the proxy can inject scripts and access contentWindow
        iframe.removeAttribute("sandbox")
    injectProxyScript(iframe)

این یک trade-off امنیتی قابل توجه است: sandboxing عمداً توسط سایت اصلی برای محدود کردن قابلیت‌های محتوای جاسازی‌شده قرار داده شده بود. حذف آن چیزی را که آن محتوا می‌تواند انجام دهد گسترش می‌دهد. این نوع تصمیمی است که برای کاربران نهایی نامرئی است اما به طور ملموس وضعیت امنیتی session پروکسی را تحت تأثیر قرار می‌دهد.


۸. پیامدهای امنیتی برای اپراتورهای پروکسی

چشم‌انداز سطح حمله

وقتی کاربران از طریق ProxyOrb مرور می‌کنند، چندین دسته از داده‌های حساس از طریق مبدأ پروکسی جریان می‌یابند:

کوکی‌های Session: از آنجایی که همه سایت‌های هدف از طریق proxyorb.com پروکسی می‌شوند، کوکی‌ها زیر آن مبدأ ذخیره می‌شوند. session‌های احراز هویت‌شده یک کاربر با بانکشان، ایمیل و شبکه‌های اجتماعی همه کوکی‌هایی زیر یک دامنه هستند. credentials: 'include' در Service Worker یعنی این کوکی‌ها با هر درخواست پروکسی‌شده همراهی می‌کنند.

هدرهای Referer: هدر Referer ارسال‌شده به سرورهای upstream نشان می‌دهد کاربر چه صفحه‌ای را مشاهده می‌کرد. پروکسی دامنه خودش را قبل از فوروارد به سرور هدف از مقادیر referer به دقت حذف می‌کند. اگر درخواستی از https://proxyorb.com/some/path?__pot=... سرچشمه بگیرد، هدر Referer خروجی به عنوان URL هدف اصلی بازسازی می‌شود (https://example.com/some/path) — دامنه پروکسی هرگز به سرورهای upstream لو نمی‌رود.

-- Pseudocode: normalize referer before forwarding upstream

function sanitizeReferer(referer):
    if referer is a proxy URL:
        return reconstruct_original_url(referer)   -- e.g. "https://example.com/path"
    if referer's origin equals proxy_origin:
        return ""   -- suppress: don't leak proxy domain to upstream
    return referer

Context اجرای JavaScript: هر فایل JavaScript ارائه‌شده از طریق پروکسی اصلاح می‌شود (URL‌ها بازنویسی می‌شوند) و در مبدأ پروکسی اجرا می‌شود. یک اسکریپت مخرب از evil.example.com که از طریق ProxyOrb پروکسی می‌شود در مبدأ proxyorb.com اجرا شده و دسترسی کامل به local storage، کوکی‌ها و IndexedDB آن مبدأ دارد — که شامل داده‌های هر سایت هدف دیگری است که کاربر از طریق پروکسی به آن دسترسی داشته.

مدل تهدید پروکسی مخرب

برای اهداف آموزشی، ارزش دارد صریح باشیم که یک پروکسی مخرب می‌توانست چه کاری انجام دهد که ProxyOrb عمداً انجام نمی‌دهد:

  1. جمع‌آوری Credential: یک پروکسی مخرب می‌تواند هر بدنه درخواست (حاوی پسوردها و داده‌های فرم) را هنگام عبور از gateway ثبت کند.

  2. ربودن Session: از آنجایی که همه کوکی‌های سایت هدف زیر دامنه پروکسی ذخیره می‌شوند، یک اپراتور پروکسی مخرب می‌تواند به آن کوکی‌ها از سمت سرور از طریق gateway دسترسی داشته باشد.

  3. تزریق محتوا: پروکسی لزوماً محتوای صفحه را اصلاح می‌کند (برای تزریق Service Worker و بازنویسی URL‌ها). یک پروکسی مخرب می‌تواند JavaScript دلخواه — keyloggerها، crypto minerها، اسکریپت‌های تقلب تبلیغاتی — نامرئی برای کاربر تزریق کند.

  4. SSL Stripping: اگر پروکسی اتصالات HTTPS را برای پای gateway-به-کاربر به HTTP کاهش دهد، همه ترافیک متن ساده است.

ProxyOrb از ابتدا تا انتها از طریق HTTPS کار می‌کند و بدنه درخواست‌ها را ثبت نمی‌کند. اپراتور هر پروکسی وب این قابلیت‌ها را دارد، به همین دلیل انتخاب یک اپراتور پروکسی قابل اعتماد به اندازه انتخاب یک ارائه‌دهنده VPN قابل اعتماد اهمیت دارد.

Mixed Content

مرورگرهای مدرن بلوک محتوای مختلط را اجرا می‌کنند: یک صفحه HTTPS نمی‌تواند sub-resourceهای HTTP لود کند. ProxyOrb این را با اجرای HTTPS روی همه اتصالات خروجی از gateway، حتی وقتی URL هدف از HTTP استفاده می‌کند، مدیریت می‌کند. override CSP شامل upgrade-insecure-requests است تا به مرورگر دستور دهد هر URL sub-resource HTTP را قبل از لود به HTTPS ارتقا دهد.

این به طور کلی مطلوب است، اما می‌تواند سایت‌هایی را که عمداً منابع را از طریق HTTP ارائه می‌دهند (CDNهای قدیمی، منابع localhost و غیره) بشکند.


۹. مسابقه تسلیحاتی جاری: نتیجه‌گیری

وقتی شروع به ساختن ProxyOrb کردیم، چالش‌های سازگاری اصلی CORS و X-Frame-Options بودند. از آن زمان، Chromium CORB، CORP، COEP، COOP را منتشر کرده و Document-Isolation-Policy را توسعه می‌دهد. جهت حرکت واضح است: مرورگرها در اجرای مرزهای cross-origin تهاجمی‌تر می‌شوند و هر مکانیزم جدید نیاز دارد که زیرساخت پروکسی خود را تطبیق دهد.

مهم‌ترین چالش پیش رو احتمالاً استقرار کامل COEP در برنامه‌های وب اصلی است. سایت‌هایی که از SharedArrayBuffer برای performance استفاده می‌کنند (ویرایشگرهای ویدیو، IDE‌ها، ابزارهای همکاری) به طور فزاینده‌ای COEP: require-corp تنظیم می‌کنند. با اینکه ProxyOrb این هدر را برای حفظ سازگاری حذف می‌کند، کاربرانی که به ویژگی‌های پرفورمنس بالایی که COEP فعال می‌کند نیاز دارند آنها را در context پروکسی تخریب‌شده می‌یابند.

برای محققان امنیتی، معماری پروکسی چیز مهمی را آشکار می‌کند: سیاست همان‌مبدأ یک فایروال نیست. این یک مدل اعتماد بر اساس ساختار URL است. پروکسی‌های وب از این واقعیت بهره می‌برند که SOP هیچ تمایز ذاتی بین «این دو منبع به طور مشروع همان‌مبدأ هستند» و «این دو منبع برای به نظر رسیدن همان‌مبدأ URL-rewrite شده‌اند» قائل نمی‌شود. کل stack امنیتی مرورگر بالای SOP — CORS، CORB، CORP، COEP، COOP — می‌توان دید که تلاشی برای اضافه کردن دفاع‌هایی است که نسبت به هویت مبدأ مبتنی بر URL مستحکم‌تر هستند.

درک این موضوع برای هر کسی که خدمات پروکسی را ممیزی می‌کند، سیاست‌های امنیتی مرورگر را طراحی می‌کند، یا وضعیت امنیتی واقعی برنامه‌هایی که ممکن است از طریق پروکسی وب دسترسی پیدا کنند را ارزیابی می‌کند، ضروری است.


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

آیا یک پروکسی وب سیاست همان‌مبدأ را دور می‌زند؟

نه دقیقاً. یک پروکسی وب با بازنویسی URL کار می‌کند: همه URL‌های cross-origin را به URL‌های همان‌مبدأ تبدیل می‌کند و محدودیت‌های cross-origin SOP را بی‌ربط می‌سازد نه اینکه آنها را دور بزند. از دیدگاه مرورگر، به نظر می‌رسد همه محتوا از مبدأ خود پروکسی می‌آید، بنابراین هیچ مرز cross-origin عبور نمی‌شود. این از نظر معماری با یک bypass متفاوت است — SOP هنوز قوانین خود را اجرا می‌کند؛ پروکسی فقط محتوا را به گونه‌ای مهندسی می‌کند که آن قوانین هرگز فعال نشوند.

CORS چطور بر سرورهای پروکسی وب تأثیر می‌گذارد؟

CORS بر سرورهای پروکسی در دو سطح تأثیر می‌گذارد. اول، پروکسی باید درخواست‌های preflight از نوع OPTIONS را رهگیری کرده و با هدرهای CORS مجاز پاسخ دهد، چون پاسخ preflight سرور هدف واقعی (که به مبدأ هدف اشاره می‌کند) بررسی CORS مرورگر برای مبدأ پروکسی را تأمین نمی‌کند. دوم، پروکسی باید هدرهای CORS از پاسخ‌های سرور هدف را حذف کرده و با هدرهایی که به مبدأ پروکسی اشاره می‌کنند جایگزین کند. تنظیم Access-Control-Allow-Credentials: true در ترکیب با credentials: 'include' در Service Worker یعنی همه کوکی‌های مبدأ پروکسی هر درخواست پروکسی‌شده را همراهی می‌کنند.

CORB چیست و چه تأثیری بر خدمات پروکسی دارد؟

Cross-Origin Read Blocking (CORB) یک دفاع Chromium است که از ورود برخی پاسخ‌های cross-origin حساس (HTML، JSON، XML) به پروسه renderer هنگام لود شدن به عنوان sub-resourceهای cross-origin جلوگیری می‌کند. برای پروکسی‌های وب با پیکربندی صحیح، CORB به طور کلی فعال نمی‌شود چون بازنویسی URL همه درخواست‌ها را همان‌مبدأ می‌کند. با این حال، CORB می‌تواند در دوره قبل از اینکه Service Worker کاملاً ثبت شود مشکل ایجاد کند، وقتی برخی درخواست‌ها ممکن است از بازنویسی URL فرار کنند و به عنوان درخواست‌های cross-origin واقعی ارسال شوند. CORB به عنوان یک اقدام مقابله با Spectre معرفی شد و در مشخصات WHATWG Fetch تعریف شده است.

آیا پروکسی‌های وب می‌توانند به کوکی‌های HTTP-only دسترسی داشته باشند؟

خیر. کوکی‌های HttpOnly توسط سرور هدف تنظیم می‌شوند و توسط JavaScript — از جمله JavaScript اجراشونده در یک Service Worker — قابل خواندن نیستند. با این حال، gateway این کوکی‌ها را هنگام فوروارد درخواست‌ها از طرف کاربر دریافت می‌کند، چون مرورگر آنها را در هدرهای درخواست ارسال می‌کند. فلگ HttpOnly از دزدی JavaScript سمت کلاینت جلوگیری می‌کند اما از دیده شدن مقادیر کوکی توسط سرور پروکسی در حین انتقال جلوگیری نمی‌کند. این مشابه این است که HttpOnly در برابر XSS محافظت می‌کند اما در برابر رهگیری سمت سرور نه.

آیا استفاده از پروکسی وب از دیدگاه امنیت مرورگر امن است؟

به مدل تهدید بستگی دارد. از دیدگاه مرورگر، همه محتوای ارائه‌شده از طریق یک پروکسی وب در مبدأ پروکسی اجرا می‌شود، یعنی یک آسیب‌پذیری در JavaScript یک سایت پروکسی‌شده می‌تواند به طور بالقوه به داده‌های session سایت پروکسی‌شده دیگری دسترسی داشته باشد (از آنجایی که یک cookie jar و storage مبدأ را به اشتراک می‌گذارند). اپراتور پروکسی هم یک طرف مورد اعتماد با دسترسی به همه ترافیک در حین انتقال است. برای دسترسی به محتوای غیرحساس در محیط‌های محدود، ریسک معمولاً قابل قبول است. برای session‌های احراز هویت‌شده با سرویس‌های حساس (بانکداری، بهداشت و درمان، دولتی)، کاربران باید بدانند که اپراتور پروکسی از نظر فنی توانایی مشاهده آن ترافیک را دارد و باید فقط از خدمات پروکسی با سیاست‌های no-log قابل ممیزی و اقدامات عملیاتی امنیتی قوی استفاده کنند.


این مقاله معماری ProxyOrb را در اوایل سال ۲۰۲۶ منعکس می‌کند. مکانیزم‌های امنیتی مرورگر به سرعت تکامل می‌یابند؛ خوانندگان تشویق می‌شوند استاندارد WHATWG Fetch، مشخصات Content Security Policy W3C و اسناد طراحی امنیتی Chromium را برای به‌روزترین اطلاعات بررسی کنند.