跳至主要內容

同源政策與網路代理:深度資安技術分析

作者
Smith
Smith
閱讀時長
16 分鐘閱讀
發布於
2026年3月2日
同源政策與網路代理:深度資安技術分析

在建構 ProxyOrb 的過程中,我們在每一個設計決策上都碰到同一個根本性的矛盾:網路代理的運作原理,是讓瀏覽器誤以為第三方內容來自同一個來源(same-origin)。這在本質上就是在欺騙瀏覽器的核心安全模型。然而,現代瀏覽器過去十五年來卻不斷堆疊越來越精密的防禦機制,目的正是對抗這類來源混淆攻擊。

本文從第一性原理出發,深入探討這兩者之間的張力。如果你是資安研究員、滲透測試工程師,或是瀏覽器安全工程師,想搞清楚網路代理究竟如何與同源政策互動——不只是概念層面,而是深入到 HTTP 標頭、Chromium 原始碼與實際工程取捨——這篇文章就是為你寫的。

本文涵蓋:SOP 基礎原理、URL 改寫機制、CORS、CORB/CORP、COEP/COOP、Service Worker、iframe,以及對代理伺服器營運者與使用者的資安意涵。


1. 同源政策的基礎

同源政策(Same-Origin Policy,SOP)由三個要素組成:協定(scheme)+主機(host)+埠號(port)。只有當三者完全吻合,兩個 URL 才算是同源。https://example.com:443http://example.com:80 儘管指向同一台伺服器,仍屬於跨域。

一般人常常搞混的是:SOP 究竟阻止了什麼,又允許了什麼。SOP 不會阻止:

  • 跨域載入圖片(<img src>
  • 跨域載入腳本(<script src>
  • 跨域載入樣式表(<link rel="stylesheet">
  • 跨域嵌入 iframe(但 iframe 內部文件的內容是隔離的)
  • 跨域送出表單(<form action>

SOP 真正阻止的是:透過 fetch()XMLHttpRequest 發出的跨域請求,其回應無法被讀取。請求確實會送出去——網路往返確實發生——但回應主體與回應標頭會被攔截,無法讓不同來源的 JavaScript 讀取。

這個區別對網路代理來說至關重要。代理的核心任務,就是將兩個來源(代理伺服器與目標伺服器)合而為一,消除跨域邊界。一旦所有資源看起來都來自 https://proxyorb.com,瀏覽器基於 SOP 對讀取回應的限制,根本就不會觸發。

網路代理利用的「合法空間」正是 URL 改寫:將 https://example.com/api/data 改寫為 https://proxyorb.com/api/data?__pot=aHR0cHM6Ly9leGFtcGxlLmNvbQ==。從瀏覽器的角度,這是一個同源請求。SOP 沒有被繞過——而是因為所有內容的表面來源都已被改變,SOP 根本派不上用場。


2. ProxyOrb 的 URL 改寫架構

要理解這套機制的資安意涵,必須先搞懂編碼方式。ProxyOrb 使用一個名為 __pot(proxy origin token,代理來源令牌)的 URL 參數,其中存放的是目標來源的 Base64 編碼。

__pot 參數

__pot 參數編碼的永遠是目標的來源(協定+主機,不含路徑),而非完整 URL。這意味著 https://example.com/some/deep/path?foo=barhttps://example.com/other/page 會產生相同的 __pot 值:aHR0cHM6Ly9leGFtcGxlLmNvbQ==(即 https://example.com 的 Base64 編碼)。實際的路徑與查詢字串則原封不動地保留在代理 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

接著,閘道器會將請求轉發至真正的目標伺服器,並改寫 Origin 標頭,讓上游伺服器看到的是自己的域名,而非代理域名:

-- 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

透過 Service Worker 進行用戶端 URL 改寫

閘道器負責伺服器端的處理。用戶端的部分——將 HTML、JavaScript 與 CSS 回應中的 URL 改寫,確保所有子請求都繼續透過代理——則由 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 都指向代理來源,瀏覽器就永遠不會發出跨域請求——每一個請求在構造上都是同源的。

在沒有完整導航的情況下還原 __pot

有一個微妙的邊界案例:從被代理頁面內部發出的請求,可能缺少 __pot 參數——例如,在 Service Worker 完成 URL 改寫之前就觸發的相對路徑 fetch('/api/data'),或是由動態注入的腳本發出、繞過了 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

閘道器也有平行的還原路徑:如果一個請求沒有帶 __pot,但其 Referer 標頭指向同源的 URL 且包含 __pot,閘道器就會從 referer 中提取令牌並附加到當前請求上。這處理了令牌在閘道器收到請求之前就被頁面本身的 JavaScript 移除的導航情境。


3. CORS:明確的跨域授權系統

CORS(Cross-Origin Resource Sharing,跨域資源共享)的設計初衷,是讓伺服器透過特定的回應標頭,主動選擇允許跨域存取。從代理的角度來看,CORS 帶來兩個截然不同的問題。

問題一:CORS 預檢請求的攔截

當頁面上的 JavaScript 使用 fetch() 發出帶有非簡單標頭的請求(例如 AuthorizationContent-Type: application/json),瀏覽器會先送出一個 OPTIONS 預檢請求。如果目標伺服器的預檢回應不包含許可的 CORS 標頭,真正的請求就會被封鎖。

在 ProxyOrb 的架構中,Service Worker 會攔截所有 OPTIONS 請求,並在請求甚至還沒到達閘道器之前,合成一個回應——以解除對真實請求的封鎖:

-- 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",
    })

在閘道器層面,每一個代理回應都會加上對應的 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"

問題二:帶憑證的請求

Access-Control-Allow-Credentials: true 搭配非萬用字元的 Access-Control-Allow-Origin,是很有份量的組合。CORS 規範要求:只要請求包含憑證,就必須明確指定來源值(不能用 *)。Service Worker 發出的所有對外請求都帶有 credentials: 'include',因此閘道器必須回傳精確的請求來源,而非萬用字元。

這意味著儲存在 proxyorb.com 下的所有 Cookie——從使用者透過代理瀏覽過的每個目標網站累積而來——都會隨著每一個代理請求一起送出。這是刻意設計的(用以維持已登入網站的 Session 狀態),但同時也是第 8 節討論的重要資安考量。

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

4. CORB 與 CORP:Chromium 的強化防禦

CORS 仰賴伺服器的明確授權。Chromium 額外加入了兩個預設啟用的機制,不論 CORS 設定為何都會生效。

跨域讀取封鎖(CORB)

CORB 是 Chrome 67(2018 年)作為 Spectre 漏洞緩解措施的一部分所引入的。其核心洞見是:即使跨域回應的主體從未被 JavaScript 讀取,只要它被 renderer 進程獲取並解碼,就表示它存在於該進程的記憶體空間中。Spectre 類攻擊就可以透過旁道(side channel)將其提取出來。

CORB 阻止特定的跨域回應進入 renderer 進程。具體而言,當 <script><img> 標籤獲取跨域回應,且該回應的 Content-Typetext/htmltext/xmlapplication/json 時,Chromium 會檢查回應主體(使用 CORB 規範 中定義的 MIME 型別嗅探器),若型別符合,就在 renderer 看到之前以空回應取代。

對於網路代理而言,如果請求確實是跨域的,CORB 將是災難性的。一個透過 <script> 標籤抓取的 API 端點回傳 application/json,會靜悄悄地返回空白。然而,由於 ProxyOrb 將所有 URL 改寫為同源,CORB 的跨域條件從來不會被觸發——從瀏覽器的視角,所有回應都來自 proxyorb.com,沒有跨域可言。

CORB 真正會造成影響的時機,是在 Service Worker 完全註冊並開始攔截請求之前的過渡期。如果任何請求在這個時間窗口內逃過了 URL 改寫,CORB 可能會封鎖它。正因這個競態條件,ProxyOrb 也在主執行緒維護一個攔截器層,在任何頁面 JavaScript 執行之前,同步地修補 XMLHttpRequestfetch 與 DOM 屬性設定器——作為 Service Worker 啟動期間的後備方案。

值得一提的是,CORB 已被不透明回應封鎖(ORB)部分取代,Chromium 正在逐步採用 ORB。ORB 在維持 Spectre 防護的同時,精煉了 CORB 規則以減少誤封。對代理相容性的影響大致相似。

跨域資源政策(CORP)

CORP 定義於 Fetch 規範,允許伺服器聲明其資源只能由同源或同站(same-site)的情境載入:

Cross-Origin-Resource-Policy: same-origin

當 Chromium 在跨域子資源請求的回應中遇到此標頭,會完全封鎖該回應。這比 CORB 更具攻擊性——不論內容型別為何都適用,且無法透過 MIME 嗅探繞過。

同樣地,由於 ProxyOrb 的 URL 改寫確保所有請求從瀏覽器角度看都是同源的,目標伺服器的 CORP 標頭不會觸發封鎖。閘道器也會防禦性地從回應中移除這些標頭,處理任何未經 URL 改寫就溜過的邊界案例。

CORP 更深層的問題,其實是代理架構有助於相容性的案例:如果 https://api.example.comhttps://www.example.com 都透過代理存取,兩者都映射到同一個代理來源,因此兩者之間的 fetch 現在是真正的同源——CORP 限制在它們之間不再適用。


5. COEP 與 COOP:跨域隔離機制

從 Chrome 92(2021 年)開始,SharedArrayBuffer 的存取——以及某些效能 API 使用的高解析度計時器——被限制在已選擇跨域隔離的頁面。這個選擇需要兩個回應標頭協同運作。

跨域嵌入器政策(COEP)

Cross-Origin-Embedder-Policy: require-corp

當一個頁面送出此標頭,瀏覽器就要求該頁面載入的每一個子資源,必須符合以下其中一個條件:

  1. 是同源的,或
  2. 明確送出 Cross-Origin-Resource-Policy: cross-origin,或
  3. 對帶憑證的 fetch 有許可的 CORS 回應

對網路代理來說,問題在於:如果目標網站在其主文件上送出 COEP: require-corp,且該文件透過代理提供時保留了此標頭,瀏覽器現在就要求該頁面載入的每個子資源也必須選擇加入。任何沒有選擇加入的資源——而大多數資源都不會——都會造成網路錯誤。

跨域開啟者政策(COOP)

Cross-Origin-Opener-Policy: same-origin

COOP 控制一個頁面是否可以與跨域頁面共享瀏覽上下文群組(browsing context group)。設為 same-origin 時,跨域導航會開啟一個新的瀏覽上下文群組,切斷頁面與任何跨域開啟者之間的 window.opener 引用和 postMessage 通道。

對代理而言,COOP 的破壞性不如 COEP 直接,但仍然會破壞依賴跨域 postMessage 流程的網站(最常見的例子是 OAuth 彈出視窗流程)。

代理營運者的兩難

這是我們在 ProxyOrb 面臨的最棘手取捨:

方案 A:從回應中移除 COEP 和 COOP。

  • 優點:子資源載入正常運作;被代理的頁面不會套用這些限制。
  • 缺點:我們移除了目標網站刻意設定的安全標頭。如果該網站使用跨域隔離來保護敏感資料免受 Spectre 類攻擊,移除這些標頭會重新曝露該風險。

方案 B:保留 COEP 和 COOP。

  • 優點:尊重目標網站的安全意圖。
  • 缺點:任何未明確設定 CORP: cross-origin 的子資源都會載入失敗,導致大多數複雜的網頁應用程式無法正常運作。

ProxyOrb 目前的策略是方案 A:閘道器從回應中移除 Cross-Origin-Embedder-PolicyCross-Origin-Opener-Policy。這最大化了網站相容性。資安取捨是有意識地做出的:使用網路代理的使用者,已經接受了他們是在不同於原本部署情境的環境中執行內容。

每一個新的瀏覽器安全機制都遵循同一個模式:先以選擇性加入的方式推出(網站可以傳送標頭來獲得保護),然後逐漸成為新 API 的預設要求,最終被考慮強制執行。COEP 就走過這條路:Chrome 83 中可選、Chrome 91 中成為 SharedArrayBuffer 的必要條件。嵌入第三方內容而未事先協調的網站,發現自己的內容壞掉了。網路代理因為本質上就在仲介第三方內容,所以將這個問題放大了。

瀏覽器安全社群也意識到了這個問題。新興的 Document-Isolation-Policy 提案旨在將進程隔離與跨域隔離要求解耦,有可能讓我們在不要求所有子資源選擇加入的情況下,也能獲得 Spectre 防護。一旦該提案落地,代理與隔離標頭的相容性可能會有所改善。


6. Service Worker 作為瀏覽器端的信任層

Service Worker 不僅僅是一個最佳化手段——它在 ProxyOrb 的安全模型中是架構上不可或缺的一環。原因如下。

註冊範圍(Scope)與來源綁定

Service Worker 的 scope 必須在註冊頁面的來源之內。當 ProxyOrb 註冊其 Service Worker 時,scope 為 https://proxyorb.com/,這意味著它會攔截該來源下任何頁面的所有 fetch 請求。

這正是 URL 改寫至同源能夠運作的根本原因:一旦 Service Worker 開始控制一個用戶端,來自該用戶端的每一個網路請求——不論頁面內的 JavaScript 嘗試 fetch 什麼 URL——都會先經過 Service Worker。

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

攔截管道

當 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)

透明的標頭轉發

有一個微妙的挑戰:瀏覽器限制 JavaScript 透過 Fetch API 設定某些「禁止」的請求標頭(HostOriginReferer 等)。Service Worker 的解決方式是將受限標頭編碼進單一的自訂傳遞標頭;閘道器在轉發給目標伺服器之前,再解碼並套用它們:

-- 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")

Service Worker 作為受信任中介者的資安意涵

從資安角度來看,Service Worker 佔據了一個特權位置:它可以檢查、修改並偽造其 scope 內任何頁面發出的任何請求。對於合法的代理用途,這正是它存在的意義。但對於惡意代理而言,這將是一個極其強大的攻擊面。

這正是 Service Worker 腳本本身可信度至關重要的原因。ProxyOrb 透過 HTTPS 從自身來源提供攔截器腳本,並設有嚴格的快取控制。如果攻擊者能夠注入經過竄改的 Service Worker,他們就能攔截受影響使用者的所有流量——不只是代理流量,而是該來源下頁面發出的任何請求。


7. iframe:框架祖先(Frame-Ancestors)問題

iframe 與一般子資源面臨的是截然不同的挑戰,因為 iframe 涉及一個巢狀瀏覽上下文,有自己的導航、自己的 SOP 邊界,以及自己的嵌入限制。

X-Frame-Optionsframe-ancestors

有兩個機制阻止頁面被嵌入 iframe:

舊版: X-Frame-Options: DENYX-Frame-Options: SAMEORIGIN

現代版: Content-Security-Policy: frame-ancestors 'none'frame-ancestors 'self'

當閘道器代理的目標頁面帶有上述任一標頭時,該頁面就無法被嵌入在代理來源下的 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 帶來第二個問題:其內容雖然從代理載入,但 iframe 的瀏覽上下文並不會自動啟用 Service Worker 或主執行緒攔截器。如果被代理的頁面建立了一個 <iframe src="https://widget.example.com/...">,該 iframe URL 也必須被改寫為代理格式,且代理的攔截器腳本必須被注入到 iframe 的文件中。

iframe 攔截器在兩個層面運作:

第一層——原型修補(捕捉程式化建立的 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 後備(捕捉 DOM 插入的 iframe):

-- 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" 控制腳本執行、表單提交、跨域存取等。要讓代理注入能夠運作,iframe 需要能執行腳本並能存取父層的代理上下文。

allow-same-origin 令牌有個特別微妙之處:當它與 allow-scripts 並用時,實際上會破壞沙箱的來源隔離——iframe 會以嵌入頁面的來源執行。當它不存在時,iframe 會獲得一個唯一的不透明來源(opaque origin),這將完全破壞代理腳本注入。

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)

這是一個重大的資安取捨:沙箱是原始網站刻意放置的,用來限制嵌入內容的能力。移除它,擴展了該內容能夠執行的操作範圍。這種決策對最終使用者來說是不可見的,但實質上影響了代理 Session 的安全態勢。


8. 代理伺服器營運者的資安意涵

攻擊面全景

當使用者透過 ProxyOrb 瀏覽時,幾類敏感資料會流經代理的來源:

Session Cookie:由於所有目標網站都透過 proxyorb.com 代理,Cookie 儲存在該來源下。使用者在銀行、電子郵件和社交媒體的已認證 Session,全都是單一域名下的 Cookie。Service Worker 的 credentials: 'include' 意味著這些 Cookie 隨著每一個代理請求一起送出。

Referer 標頭:傳送給上游伺服器的 Referer 標頭,揭露了使用者正在瀏覽哪個頁面。代理在轉發給目標伺服器之前,會仔細地從 referer 值中移除自身的域名。如果一個請求來自 https://proxyorb.com/some/path?__pot=...,對外的 Referer 標頭會被重建為原始目標 URL(https://example.com/some/path)——代理域名永遠不會洩漏給上游伺服器。

-- 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

JavaScript 執行上下文:所有透過代理提供的 JavaScript 文件都經過修改(URL 改寫),並在代理的來源中執行。一個來自 evil.example.com 、透過 ProxyOrb 代理的惡意腳本,會在 proxyorb.com 來源中執行,並可完整存取該來源的 localStorage、Cookie 和 IndexedDB——這包括使用者透過代理存取過的所有其他目標網站的資料。

惡意代理的威脅模型

基於教育目的,有必要明確說明一個惡意代理可以做到、而 ProxyOrb 刻意不做的事:

  1. 憑證竊取:惡意代理可以記錄每個請求主體(包含密碼和表單資料),這些資料在通過閘道器時一覽無遺。

  2. Session 劫持:由於所有目標網站的 Cookie 都儲存在代理域名下,惡意代理營運者可以在伺服器端透過閘道器存取這些 Cookie。

  3. 內容注入:代理必然需要修改頁面內容(注入 Service Worker 並改寫 URL)。惡意代理可以注入任意 JavaScript——鍵盤記錄器、挖礦腳本、廣告詐欺腳本——對使用者完全不可見。

  4. SSL 剝除:如果代理在閘道器到使用者這段路程中,將 HTTPS 連線降級為 HTTP,所有流量都將以明文傳輸。

ProxyOrb 全程透過 HTTPS 運作,且不記錄請求主體。任何網路代理的營運者都具備上述能力,這正是為何選擇一個值得信賴的代理伺服器營運者,與選擇一個值得信賴的 VPN 提供商同樣重要

混合內容

現代瀏覽器強制執行混合內容封鎖:HTTPS 頁面無法載入 HTTP 子資源。ProxyOrb 的處理方式是強制閘道器所有對外連線使用 HTTPS,即便目標 URL 使用的是 HTTP。CSP 覆寫包含 upgrade-insecure-requests,以指示瀏覽器在載入之前將所有 HTTP 子資源 URL 升級為 HTTPS。

這通常是理想的做法,但可能會破壞刻意透過 HTTP 提供資源的網站(舊版 CDN、localhost 資源等)。


9. 持續的軍備競賽:結語

當我們開始建構 ProxyOrb 時,主要的相容性挑戰是 CORS 和 X-Frame-Options。從那時起,Chromium 陸續推出了 CORB、CORP、COEP、COOP,並正在開發 Document-Isolation-Policy。趨勢顯而易見:瀏覽器在強制執行跨域邊界這件事上越來越積極,每一個新機制都需要代理基礎設施適應調整。

目前最重要的挑戰,很可能是 COEP 在主流網頁應用程式中的全面部署。使用 SharedArrayBuffer 追求效能的網站(影片編輯器、IDE、協作工具)越來越頻繁地設定 COEP: require-corp。由於 ProxyOrb 移除此標頭以維持相容性,需要 COEP 所啟用的高效能功能的使用者,在代理情境下會發現這些功能降級了。

對資安研究員來說,代理架構揭示了一件重要的事:同源政策不是一道防火牆,而是一套基於 URL 結構的信任模型。網路代理利用的事實是:SOP 在「這兩個資源確實是同源的」與「這兩個資源被 URL 改寫成看起來像同源的」之間,沒有任何內在的區別。SOP 之上的整個瀏覽器安全堆疊——CORS、CORB、CORP、COEP、COOP——可以視為一種嘗試,要增加比基於 URL 的來源身份更強固的防禦機制。

理解這一點,對任何審計代理服務、設計瀏覽器安全政策,或評估可能透過代理存取的應用程式實際資安態勢的人來說,都是至關重要的。


常見問題

網路代理會繞過同源政策嗎?

不完全是。網路代理的運作方式是 URL 改寫:它將所有跨域 URL 轉換為同源 URL,使 SOP 的跨域限制根本不會觸發,而非真正繞過它們。從瀏覽器的角度,所有內容看起來都來自代理自身的來源,因此沒有跨域邊界被跨越。這在架構上與繞過是不同的——SOP 仍然在執行其規則;代理只是將內容工程化,使那些規則永遠不會被觸發。

CORS 如何影響網路代理伺服器?

CORS 在兩個層面影響代理伺服器。首先,代理必須攔截 OPTIONS 預檢請求,並以許可的 CORS 標頭回應,因為真實目標伺服器的預檢回應(引用目標來源)無法滿足瀏覽器對代理來源的 CORS 檢查。其次,代理必須剝除目標伺服器回應中的 CORS 標頭,並以引用代理來源的標頭取代。Access-Control-Allow-Credentials: true 設定,搭配 Service Worker 中的 credentials: 'include',意味著代理來源的所有 Cookie 都會隨每個代理請求一起送出。

CORB 是什麼?它如何影響代理服務?

跨域讀取封鎖(CORB)是 Chromium 的一項防禦機制,當敏感的跨域回應(HTML、JSON、XML)作為跨域子資源被載入時,阻止其進入 renderer 進程。對於正確設定的網路代理,CORB 通常不會被觸發,因為 URL 改寫讓所有請求都成為同源的。然而,CORB 可能在 Service Worker 完全註冊之前的期間造成問題,此時部分請求可能逃過 URL 改寫,以真正的跨域請求方式送出。CORB 作為 Spectre 漏洞的緩解措施被引入,定義於 WHATWG Fetch 規範

不行。HttpOnly Cookie 由目標伺服器設定,JavaScript 無法讀取——包括在 Service Worker 中執行的 JavaScript。然而,閘道器在代表使用者轉發請求時,會收到這些 Cookie,因為瀏覽器會在請求標頭中送出它們。HttpOnly 旗標防止了用戶端 JavaScript 的竊取,但不能防止代理伺服器本身在傳輸過程中看到 Cookie 值。這類似於 HttpOnly 能防範 XSS,但無法防範伺服器端攔截。

從瀏覽器安全角度來看,使用網路代理安全嗎?

這取決於威脅模型。從瀏覽器的角度,所有透過網路代理提供的內容都在代理的來源中執行,這意味著某個被代理網站的 JavaScript 漏洞,可能潛在地存取來自另一個被代理網站 Session 的資料(因為它們共享同一個來源的 Cookie 儲存庫與儲存空間)。代理的營運者也是一個受信任的當事方,能存取所有傳輸中的流量。對於在受限環境中存取非敏感內容,風險通常是可以接受的。對於涉及敏感服務的已認證 Session(銀行、醫療、政府),使用者應了解代理營運者在技術上具備觀察這些流量的能力,且應只使用具有可稽核的無日誌政策和強大營運安全實踐的代理服務。


本文反映 ProxyOrb 截至 2026 年初的架構。瀏覽器安全機制演進迅速;建議讀者查閱 WHATWG Fetch 標準W3C 內容安全政策規範,以及 Chromium 安全設計文件 以獲取最新資訊。