回覆列表
-
1 # 程式設計師周先生
-
2 # 老漢籃球
瀏覽器的同源策略限制了跨域訪問的行為,這主要是基於安全考慮。
那麼什麼是同源策略(same-origin policy)呢?同源策略是瀏覽器一個核心的安全技術,它限制某個域下的資源或指令碼不能訪問其它域的資源,比如限制某個網站的Javascript指令碼不能訪問其它網站的cookie資訊。
所謂的域是指協議(http/https/ftp)、域名(可以是IP或域名形式,如8.8.8.8、www.google.com)以及埠(80/443等)一樣的所有網頁,正常情況下,只要這三項一樣,網頁之間可以相互訪問。
透過同樣的方式,我還可以盜用你訪問過的任何網站賬號,想象一下這是多麼可怕的事情。所以,瀏覽其必須限制跨域訪問,這是瀏覽器安全的基礎。
背景
Ted Nelson 曾經談過文學二神 —— 讀者和作家,它們都是無敵的:作家可以想寫什麼就些什麼,而讀者可以選擇什麼也不讀。不過現在我們有三個神,讀者、作家還有中間人,它們任一方都不是無所不能的。
瀏覽器一直在做的假設是,任何時候只要使用者開始使用網際網路應用,這個應用就開始嘗試攻擊這個使用者,也就是說網際網路應用是預設不可信的,瀏覽器在它們可能做的任何事情上都加了限制,而且也對網際網路應用開發者提供有限的關於這些限制的報錯資訊。因此這是一個對程式非常不友善的環境,除非這個程式是一個很簡單的不訪問任何網際網路資料的程式。
跨站指令碼攻擊(XSS)
最早迫使瀏覽器採用不信任網際網路應用這個設計思想的攻擊方式,就是跨站點指令碼攻擊。在這種攻擊中,一個惡意的人員刀疤哥會向普通使用者愛麗絲 傳送一封電子郵件,郵件裡有一個指向刀疤哥的網站的連結。愛麗絲毫無防備心地點選連結,讓瀏覽器載入網頁刀疤網,瀏覽器允許 JavaScript 程式預設在任何網頁上執行,因此刀疤網上會有一個 JS 程式在愛麗絲的裝置上執行,訪問愛麗絲裝置上她能訪問的一些資料,然後秘密傳送資料到刀疤哥的伺服器上。
程式可以透過各種方式訪問 愛麗絲的私有資料。一種方式是愛麗絲正在使用的計算機可能位於防火牆內,該防火牆允許她進行隱式訪問她家中的網路攝像頭或她大學的期刊庫等等資源。之所以說隱式訪問,是因為這些訪問可以在沒有與愛麗絲進行任何互動的情況下完成,或者也可以透過要求她登入系統以讓程式獲得一些憑據(例如 Cookie)來完成。網站刀疤網也可能是她使用的某個銀行的官網的虛假版本,一個釣魚網站,它要求她以某種藉口向銀行或社交網路進行身份驗證。這個釣魚網站將資料返回給刀疤哥的方法有很多,例如將竊取到的資料放到刀疤網的 URL 後面,傳送 GET 請求,這樣刀疤網的伺服器就能拿到資料。跨站指令碼攻擊可能有很多變種,這裡說的是最一般的思路。
跨域資源共享(CORS)
瀏覽器開發者的第一個衝動可能就是完全阻止 JavaScript 程式進行任何網際網路訪問,這樣它們就沒法偷偷上傳使用者資料了。但顯然網際網路應用的開發者需要他們的 JS 程式能夠進行網路訪問。例如,銀行肯定需要載入一個程式,透過上傳使用者輸入的資訊來向銀行伺服器請求關於這個使用者的不同日期、不同賬戶的更多資料。顯然必須允許上傳資料才能實現這些互動。
但我們不會允許這些資料被上傳到刀疤哥的伺服器上,這就是同源策略,只要程式在同一個網際網路域名(如 http://www.icbc.com.cn )的網頁中執行,那麼程式可以與這個域名下的任何伺服器地址進行互動。URI 中的協議和域名就組成了我們所說的「源」。因此同源政策(Same Origin Policy, SOP)說的就是,來自某一個伺服器「源」的資料,以及來自這個伺服器的程式的資料,都與來自任何其他源的任何資料分開。這使銀行的程式能夠很好地運作,又不會暴露隱私。
有什麼場景之中同源策略是不可用嗎?這還是有的,任何需要程式去訪問其他域名下的資料的場景都會被同源策略阻礙到。例如如果有一個網站想提供一個 JavaScript 程式來檢驗、測試或者僅僅訪問另一個網頁,那麼它沒法訪問那個網站上的資料,也就沒法達到它的設計目的。
另一個例子是資料融合。當政府開始公開大量的開放公共資料時,有一些網站會開始湧現,這些網站從許多不同的開放資料站點載入資料並提供資料的「融合」—— 組合許多不同來源的資料,並提供視覺化,從而讓使用者享受到單一資料來源所不能提供的洞察力。但實際上,典型的純前端的資料融合網站現在已經無法工作了。
那麼有什麼替代方案?瀏覽器製造商實現了一些鉤子以允許資料在不同的源上共享,並稱之為跨域資源共享(CORS)。核心問題是 —— 如何在瀏覽器中區分資料,區分私人的網路攝像頭資料和公開的政府開放資料?我們無法改變網路攝像頭,但我們可以改變開放資料釋出者。瀏覽器製造商現在要求開放資料釋出者在 HTTP 響應中為任何完全開放的資料新增特殊的 CORS 頭:
Access-control-allow-Origin: *
例如銀行可以允許可信的信用卡公司的程式訪問銀行的源下的使用者資料,這可以使得運營銀行變得更容易:
Access-control-allow-Origin: credit card company.example.com
這意味著釋出公開資料的人需要在他們的任何 HTTP 響應里加上
Access-control-allow-Origin: *
這意味著給網際網路上隨機的開放資料者帶來大量的工作量,可能這些開放資料提供方會因為各種原因沒法給所有響應都加上這一條響應頭。這使得他們的資料只能被瀏覽器直接訪問,而沒法被網際網路應用利用。
瀏覽器事實上不直接檢視這些響應頭,而是在一個前驅(Pre-flight)的 OPTIONS 請求裡檢視,這個請求會自動插入到其他主要的請求之前。所以當開發者在開發者工具裡看到主要的請求時,其實已經有幾輪請求發生了。
響應頭攔截
除了阻止訪問資料以外,CORS 系統還會阻止不同源的伺服器的響應頭髮送給網際網路應用。如果不想被阻止,伺服器必須加上另一個響應頭:
Access-Control-Allow-Headers: Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, WAC-Allow, Content-Length, WWW-Authenticate上述的響應頭裡必須包含一些東西,比如「Link」。這些是一般會被瀏覽器阻止的響應頭,你也可以把任何其他的應用和伺服器需要因其他目的而是用的響應頭加進去。
HTTP 方法攔截
作為習題留給讀者思考。
例子
官方的示例 SoLiD 伺服器透過這種方式來允許跨域資源訪問。
對 CORS 的調整
這裡我們要說的調整是:CORS 的設計者事實上故意使其變得更加難使用。
有人會一種感覺,我明白,就是如果允許資料釋出者簡單地把 ACAO:* 加在在他們釋出的內容上,這會是一個,讓 使用者很容易自廢武功的設計。
這裡的「使用者」當然不是普通使用者,而是指配置網路伺服器的系統管理員。我們擔心的是系統管理員會發現瀏覽器封鎖了對其資料的訪問,為了解決這個問題,他們只會在任何地方都加上這個響應頭,即使某些資料實際上並不應該被公開。例如,他們提供了不同版本的頁面給不同的使用者,此時保持使用者之間的資料隔離是很重要的,但他們會受到誘惑用ACAO:*來把所有資料都標識成可以訪問的。
因此,只要傳入的請求中帶有使用者憑據資訊,瀏覽器就會阻止伺服器使用 ACAO:*。 每當使用者「登入」時,如果你願意,明確地使用他們登入時上傳的身份資訊。
但是,有時系統需要訪問來自另一個源的使用者私有的資訊,例如前述的銀行的例子。對於這種使用憑據的情況,只允許訪問 Access-Control-Allow-Origin 響應頭中明確指定了的源。
麻煩的是 HTTPS:網際網路現在分為兩個網路,一個是我們用來登入和傳遞憑證的網路,而另一個是低安全性的網路。問題在於,如果你開發的不是終端使用者頂級應用程式,而是一箇中間件,一個程式碼庫,你只能呼叫瀏覽器來做網路操作,以及用於處理密碼或 TLS 的瀏覽器 API,必要時得進行登入。中介軟體的程式碼沒法知道整個過程。
這意味著,如果你的伺服器釋出的是完全公開的資料,例如政府開放資料,你希望任何程式碼都能夠訪問你的資料,系統管理員之間的經驗法則是你應該總是迴應任何請求頭相同的源。相比於用這個:
Access-control-allow-origin: *
所有的開放資料伺服器應該傳送這個:
Access-control-allow-origin: $(RECEIVED_ORIGIN)
此處的 $(RECEIVED_ORIGIN) 就用請求頭中的源來替換。
這可能會比向所有公共資料伺服器新增固定欄位更復雜。不過這是一個進步,可以讓程式碼來幹活,而不是簡單地讓人去改改配置 —— 這需要讓每一個開放資料釋出者都配合。
要向資料釋出者解釋清楚這些東西簡直像打一場戰役,戰役的結果是到處都可以搜到這些程式碼片段。事實上 Apache 都把這個搞成了一個內部的環境變數[@@ref]
難道沒有更好的設計來設定靜態標頭嗎,例如
Access-control-allow-origin: PUBLIC_AND_UNCUSTOMIZED
這樣系統管理員就不會把一些私密的東西隨便暴露出去了,或者也可以用使用者的身份來定製化?可能有的人會這麼想。不過這就是現在 CORS 實現的方式。
所以,世界上的資料釋出者都開始把 CORS 源配置透過反射新增到響應頭裡了。
但一旦你要使用反射式加源的響應頭,很關鍵的一點事允許把 Origin 加到 Vary: 響應頭裡,如果你有 Vary: 的話。如果沒有,就加上一個:
Vary: Origin到每一個透過反射來配置 ACAO 的響應頭裡。
不然的話,這裡有一個失敗的例子:
站點A上的程式向伺服器請求公共開放資料
伺服器使用 ACAO 響應頭頭響應資料
瀏覽器快取該響應
使用者使用站點B上的其他程式檢視相同的資料
瀏覽器使用快取副本,但其上的源A與請求站點B不匹配。
瀏覽器以靜默方式阻止了請求,使用者和開發者感到十分費解
因此,在正常執行的基於 CORS 的系統中,伺服器傳送 Vary:Origin 響應頭,並強制瀏覽器為每個請求它的 Web 應用程式保留不同的資料副本,這非常具有諷刺意味,因為這些資料副本可能是完全公開的資料,不需要做任何隱私考量。
CORS設計在歷史上大多數時候都是整個網路中最糟糕的設計。但現在,那些設計像 Solid 這樣的系統的人必須建立一個不受 XSS 攻擊影響的系統,資料將是在使用者的控制下完全對外公開或者完全對外不可見,並且 Web 應用程式將被各種不同的社交流程視為可信賴的。
後記: 對CORS的第二次調整
對CORS的第二次調整是在瀏覽器還沒完全實現完第一次調整的時候發生的。
Notwithstanding issues with the design of CORS, Chrome doesn’t in fact do it properly.
If you request the same resource first from one origin and then from another, it serves the cached version, which then fails cord because the Origin and access-control-allow-origin headers don’t match. This even when the returned headers have “Vary: Origin”, which should prevent that same cached version being reused for a different origin.
儘管 CORS 的設計存在問題,但 Chrome 實際上也並沒有正確地實現它。如果你首先從一個源請求相同的資源,然後從另一個源請求相同的資源,瀏覽器將嘗試提供快取版本,然後由於 Origin 和access-control-allow-origin 響應頭不匹配而失敗。即使返回的響應頭具有 Vary:Origin,這也應該防止相同的快取版本被重用於不同的源。
問題出現於 Chrome Version 59.0.3071.115 (Official Build) (64-bit)
火狐在 2018-07 也出了同樣的問題。