對於網頁學習者來說,同源政策(Same Origin Policy)可能是一個很重要的困惑點。
當我們終於搞懂了client與server;理解了HTTP request/response;摸熟了AJAX;想出了一個絕佳的專案點子⋯⋯但不知為何,API的資料就是讀不出來。
於是我們開始研究關鍵字「同源政策」並持續Google,每次都好像又多懂了一點,但又不是那麼肯定——特別是在部署至production環境時,只要網域轉換,似乎就會發生不可預期的效果。
讓我們試著一次把它搞懂吧。
圖片:Ajax迷因。Same Origin Policy是件麻煩的事(出處:reddit)
瀏覽器/使用者代理人/安全性
要理解同源政策,最核心的重點就是理解:
同源政策是由「誰」來執行?以及「為什麼」?
是HTTP(S)的嗎?還是JavaScript?是瀏覽器嗎?是網站框架嗎?還是TCP/IP?
如果能將以下重點銘記在心,我想在閱讀文件時就會感到豁然開朗:
同源政策,Same Origin Policy,是「瀏覽器」作為使用者代理人,為了「安全性考量」,所設定的規範。
什麼是使用者代理人?
請試著想像如下情境:我們拿著雙證件到銀行開了戶,也存了錢進去。行員告知:如果需要匯款或是其他操作,可以透過帳號+密碼登入銀行網站,在瀏覽器中進行——這樣就不用每次都跑銀行囉。
在現實世界裡,與我們互動的是銀行行員,我們可以透過證件與存摺存取帳戶;但在網路銀行中,與我們互動的對象成了Web server,而驗證身份的方式則變成帳號+密碼。
當我們請銀行行員協助時,使用的是人類的自然語言。但在網路銀行中,我們與web server是透過使用者介面/通訊協定/網路訊號來進行溝通的。
但作為一個人類,我們無法用大腦解析這一切。
於是我們需要一個代理人(user agent)——也就是瀏覽器,協助我們與web server進行溝通。這包含了渲染UI、設定Cookie、執行TCP三向交握、通訊加密、並將我們的表單解析成Http Request後送出⋯⋯等。
使用者的「安全性」是瀏覽器的職責
在操作的過程裡,瀏覽器有義務保護使用者的資料——尤其是涉及身份驗證的部分。
我們會透過瀏覽器登入各種帳號(例如網路銀行、臉書、Gmail、以及各式各樣來路不明的網站)。當成功使用帳號密碼登入時,這些服務的Web server會透過HTTP header, 提供一段auth token並要求瀏覽器存在cookie裡。在auth token的有效期間內,我們的http request都會自動帶上這段cookie,以此為憑,我們因此可以對服務進行各種操作,例如匯款、貼文、按讚、回覆email⋯⋯等。
但問題來了,如果有一個惡意的盜版漫畫網站evil.com
,當我們在訪問時,透過頁面中JS以及AJAX,偷偷在瀏覽器中執行這樣的行為呢:
「請把銀行bank.com
的cookie資料(包含auth token)讀取出來;透過AJAX+POST;回傳到https://evil.com/sniff
」
當然,作為一個使用者代理人,瀏覽器會拒絕如是請求。
拒絕的原則是什麼呢?那就是同源政策最重要的一環:「非同源的資料讀取是不被允許的」。既然evil.com
不能讀取bank.com
設定的cookie,那當然也無法將之回傳。
同源(Same-origin)的定義
如果能充分了解以上原則。剩下的,就只是技術細節的問題。
我們可以透過火狐家的MDN文件理解同源政策的規範。一言以敝之,我們可以把origin視做一個three-tuple(scheme, host, port)
,也就是(通訊協定, 網域, 埠號)
的組合。兩個document的origin只要任一欄位不相同,就會被瀏覽器視作不同源[1]。
舉例如下:
- 以下二者同源
- 以下二者不同源,因為通訊協定不同(HTTPS與HTTP)
- 以下二者不同源,因為網域不同[2]
- 以下二者不同源,因為埠號不同(80與8080)
此外,同源政策是以document為單位的。舉例來說:假設store.mystie.com
的document透過<script>
標籤取得了來自cdn.google.com
的JS腳本,則該腳本的origin仍是由blog.mystie.com
來定義。
在實務中,我們最常遇到的origin變數往往是網域。為了方便起見,以下討論會將網域等同於origin使用,但請注意origin包含的不只有網域而已。
圖片:我們可以透過document.domain來取得文件所屬的網域。
跨源存取細節
討論到這裡,可能我們已經想到一堆例外了⋯⋯
- 我的網站會透過CDN導入jQuery函式庫和Bootstrap的CSS,為什麼這不受限制?
- 我的網站會使用
<img>
讀取外站圖片,為什麼這不受限制?
事實上,同源政策其實有許多細節,甚至各家瀏覽器的實作都會有些微不同。請注意,當我們在討論這個問題時,我們的角色已經轉變成服務的提供者與Web Server了。在研究同源政策時,釐清主詞(使用者/瀏覽器/網站後端)以及行為(request/response)是最重要的起手。
我們可以透過MDN的文件理解不同的跨源行為:
跨源寫入(write)原則上允許
可以在A網域的document中提交請求給B網站;也可以透過超連結、轉址等方式連結至B網站。
跨源嵌入(embed)原則上允許
可以使用<img> <video> <iframe>
標籤嵌入來自其他網域的資源。
可以使用<link rel="stylesheet" href="…">
套用來自其他網域的CSS。
可以使用<scrip src="">
標籤嵌入來自其他網域的腳本[3]
跨源讀取(read)原則上禁止
A網域的document不能讀取B網域的本地端儲存資料(例如Cookie)與回應。
值得注意的是:我們可以在A網域的document中,使用JS發送AJAX Request給B(跨源寫入允許);但就算B有回應;文件也無法讀取response(跨源讀取禁止)。例如這樣的jQuery操作$.get("https://www.google.com ")
在所有非Google的document中,都會回報disallows reading
的錯誤。
那第三方的Client端API是怎麼實作的?
如果跨源讀取是被禁止的,作為一個開發者(假設是交友網站),如果我們希望有一個第三方API服務(例如臉書),可以讓我們透過瀏覽器+AJAX取得資料,這其中的實作原理是什麼呢?
實情是:臉書作為第三方服務的提供者(務必注意主詞!),在回應請求時,是可以透過HTTP header中的Access-Control-Allow-Origin
欄位,請瀏覽器特別通融。
舉例來說,當我們的交友網站向臉書申請API帳號時,臉書會提供一組token並詢問我方網域(假設是https://friend.com
)。當我們使用這組token請求資料時,臉書就會在response中設定Access-Control-Allow-Origin=https://friend.com
,翻成白話就是:
雖然 friend.com 與我不同源,但請准許他讀取這則HTTP response。
當收到這樣的回應時,瀏覽器就會允許friend.com
之下的document讀取來自facebook API的回應囉[4]。
如果API server將Access-Control-Allow-Origin
設定為*
時,代表任何origin都可以讀取回覆,不受同源政策限制。
當然,回到一開始銀行的例子。基於安全性考量(只賦予必要的最小權限),當然就沒有必要設定這個欄位了。
圖片:使用client端API時,設定網域通常是必要的
那麼⋯⋯我們就這樣高枕無憂了嗎?
與同源政策常常一起討論,且觀念容易糾纏在一起的另一個主題,是所謂的跨域請求偽造攻擊(Cross Site Request Forgery)。這兩者彼此概念交錯,但其實同源政策並不能完全防止跨域請求偽造攻擊——不過這也是另一個主題了,我們就有空再說吧。
目前為止,希望我們已經清楚解釋了同源政策的大概念。
註解
[1] 在RFC-6454原文中的host對應的中文應該是「主機名稱」。但本篇文章主要聚焦於瀏覽器的一般用例(而非各種形式的user agent),因此使用「網域」一詞雖然不完全精準,但為了可讀性仍然選用。
[2] cookie的讀取權限可以下放給子網域。細節請見RFC-6256中的Domain Matching章節。
[3] 請注意這包含了「伺服器端後果自負」的前提。作為後端服務的開發者,在document中嵌入來自第三方(諸如Google CDN)的JavaScript意即「允許第三方腳本以我的origin身份進行操作」——這是另一個網頁安全性的大主題,有興趣的讀者可以搜尋Cross Site Scripting(XSS)。
[4] 請注意這只是允許讀取response,並不包含允許friend.com讀取facebook.com的cookie的意味。
[ref] IETF | RFC-6265 HTTP State Management Mechanism
[ref] IETF | RFC-6454 The Web Origin Concept
[ref] LiveOverflow | CSRF Introduction and what is the Same-Origin Policy? Concept
[ref] MDN | Same Origin Policy
[ref] stackoverflow | Does including all these 3rd party javascript files impose a security risk?