Skip to content

[Security] CORS 與跨來源資源安全性問題

· 29 min

前言#

承接上篇 [Security] 細究 Same-origin policy 與 same site,此篇主要敘述的是《Beyond XSS:探索網頁前端資安宇宙》 4–2 ~ 4–3 章節的筆記,若有錯誤歡迎大家回覆告訴我~

4–1 ~ 4–3 章節剛好也是我在讀書會負責導讀的章節,有另外做一份簡報,也可以參考看看簡報,雖然內容大同小異XD,簡報連結點此🔗

CORS 基本介紹#

同源政策 same-origin policy 意思是瀏覽器會阻止一個網站讀取另一個不同來源網站的資料,但開發時很常遇到一個問題,當前後端在不同 origin 時,前端該如何讀到後端資料?舉例來說前端在 monica.tw、後端在 api.monica.tw,此時解法是 CORS(Cross-Origin Resource Sharing),CORS 是一種可跨來源交換網站資料的機制,開發時很常使用,不過如果設定錯誤就會變成資安漏洞。

為什麼不能跨來源呼叫 API?#

精確來說應該是「為什麼不能用 XMLHttpRequestfetch(也可簡稱為 AJAX)獲取跨來源資料?」

當我們用 imgscript 請求跨來源資源時並不會遇到問題,但 AJAX 請求卻會被阻擋,例如 <img src="https://another-domain.com/bg.png" /> 可順利拿到圖片。

此時可用反向思考法,當我們已知結果,則此結果一定有原因,可用反證法,若 A 結果是對的,就先假設 A 是錯的,再找出反例發現矛盾,最後證明 A 是對的。因此先假設「擋住跨來源請求」是錯的,再來思考反例,就能發現「擋住跨來源請求」其實是必要的。也可參考 Huli 在這篇文章提到,可以用三個問題來幫助自己理解一項事物:

  1. 為什麼要有 XXX?
  2. 沒有 XXX 跟有 XXX 的區別是什麼?
  3. 所以 XXX 是什麼?

先來思考,如果跨來源請求不會被擋住,會發生什麼事?

我們可以自由串 API,不受任何阻擋,可在自己網域網頁 https://monica.tw/index.html 用 AJAX 拿 https://google.com 資料,非常開心🥳。

可是有些問題,以下來看問題舉例 1:

  1. 目標(受害者)用公司員工電腦打開惡意網站
  2. 惡意網站發送 AJAX 請求,取得內部機密網站資料
  3. 惡意網站拿到機密資料
  4. 惡意網站將資料回傳給攻擊者 server

也附上示意圖如下。

不過這攻擊有侷限性,因為攻擊者需要先知道內部網站網址,才能攻擊。

問題舉例 2 則是我們平常開發常見的情況,平常開發時很常會在自己電腦開 server,網址如:http://localhost:3000http://localhost:5566。而如果瀏覽器沒阻擋跨來源 API,攻擊者可這樣拿到 localhost server 上的資料:

function sendRequest(url, callback) {
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function() {
callback(this.response);
}
request.send();
}
// 嘗試針對每一個 port 拿資料,拿到就送回去自己(攻擊者) server
for (let port = 80; port < 10000; port++) {
sendRequest('http://localhost:' + port, data => {
// 把資料送回 server
})
}

localhost 上可取得的資料可能是正在開發的公司機密,也可能是可分析網站漏洞的資料,用來做進一步攻擊。

問題舉例 3 則是假設跨來源請求會自動附上 cookie,這樣當使用者瀏覽惡意網站時,惡意網站就可以發 request 到 https://www.facebook.com/messages/thttps://mail.google.com/mail/u/0/,因為會自動帶上使用者 cookie,就能拿到使用者隱私資料。

看完這些問題,再思考一次「為什麼要擋住跨來源的 AJAX?」,答案就是「安全性」,瀏覽器若要拿網站完整內容(可完整讀取),只能用 XMLHttpRequestfetch,若沒限制跨來源 AJAX,就能透過使用者瀏覽器拿到「任意網站」內容,任意網站也包含可能有敏感資訊的網站。

那為什麼不擋圖片、CSS 或 script? 因為用 <img> 引入圖片、用 <link href="..."> 拿 CSS 屬於「網頁資源的一部分」,標籤可拿到的資源有限制,且取回來的資源無法用程式讀取,載入圖片後就只是圖片,只有瀏覽器知道圖片內容,無法用程式讀取代表無法將結果傳到其他地方,比較不會有資料外洩問題。

跨來源 AJAX 是怎麼被擋掉的?#

先簡單敘述個小情境:小明在開發專案時,要使用一個刪除文章的 API,用法是 POST 並帶上文章 id,content type 是 application/x-www-form-urlencoded,另外公司前後端網域不同,且後端沒加 CORS 的 header,小明呼叫刪除文章 API 後,console 跳錯:「request has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource」。因此小明認為:「因同源政策限制,AJAX 發不出請求,文章刪不掉」,此說法正確嗎?

小明的故事其實有個很大的誤解,他認為「跨來源請求擋住的是 request」,此想法有誤,我們再看一次錯誤訊息:

request has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource

訊息說的是請求的資源沒有 ‘Access-Control-Allow-Origin’ 這 header 存在,代表瀏覽器已經發出 request、拿到 response 才發現沒有 ‘Access-Control-Allow-Origin’ 的 header 存在。因此瀏覽器擋住的不是 request,而是 response(只適用簡單請求),request 已到伺服器,瀏覽器也收到 response,只是瀏覽器不把結果給你。上面小明的故事中,小明的 request 已經到 server,文章已被刪除,只是小明拿不到 response。

有人有「跨來源請求擋住的是 request」的類似誤解,以為發出的請求不會到 server 而向 Chromium 回報 issue,例如 Issue 1122756: Possible to send XHR POST request from different origins — SOP bypassIssue 1151540: Same-Origin-Policy is bypassed by an XMLHttpRequest Executed within an eval(),這些都被標記不會修,因為這符合規範實作。


可能有人會困惑,脫離瀏覽器就沒 CORS 問題,那攻擊者是不是可以在瀏覽器以外環境向其他機密網站拿資料?在瀏覽器以外環境請求如:用 curl 或 Postman 等工具,就不被 CORS 限制。

但是在瀏覽器發請求與脫離瀏覽器發請求,兩者有根本差異:

兩者最大的差異是在哪個地方(誰的電腦)造訪網站、發出請求,前者是在自己電腦發請求,後者是在其他有連到內網的人電腦發請求。示意圖如下,圖上半是攻擊者自己用 curl連內網網址會失敗,即使沒有 same-origin policy,圖下半是攻擊者寫個惡意網站,讓使用者造訪網站,之後用 AJAX 發 request 到攻擊目標(internal server),拿到資料後再回傳給攻擊者(步驟 4),而如果有 same-origin policy,步驟 4 就不成立,因 JavaScript 拿不到 fetch 結果,不知道 response 內容。

如何設置 CORS?#

設 CORS 的方式就是在伺服器回傳 response 時,在 header 和瀏覽器說:「允許 XXX 存取這請求的 response」。

Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://blog.huli.tw

目前 Access-Control-Allow-Origin 的值不支援多個 origin,只能在伺服器根據 request 動態處理不同 header。

跨來源請求分為「簡單請求」跟「非簡單請求」(ref:Cross-Origin Resource Sharing (CORS)),不論哪種,後端都要給 Access-Control-Allow-Origin header,另外,非簡單請求在發正式請求前會先發一個 preflight request,若 preflight 沒通過就不發正式請求,preflight 請求也要給 Access-Control-Allow-Origin header。簡單來說,簡單請求、非簡單請求的正式請求、非簡單請求的 preflight request 都需要後端給 Access-Control-Allow-Origin header。

若要傳送自定義 header,後端要新增 Access-Control-Allow-Headers 才能通過 preflight,範例如下:

// 假設想帶上自定義 header X-App-Version 紀錄網站版本
fetch('http://localhost:3000/form', {
method: 'POST',
headers: {
'X-App-Version': "v1.1",
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(res => res.json())
.then(res => console.log(res))

後端回應 preflight 請求時要新增 Access-Control-Allow-Headers

app.options('/form', (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'X-App-Version, content-type')
res.end()
})

preflight 可視為一種驗證機制,確保後端知道前端送出的 request 符合預期,瀏覽器才會讓前端發出正式 request,因此前面說的「跨來源請求擋的是 response 而不是 request」只適用簡單請求,有 preflight 的非簡單請求,真正的請求會被阻擋。
總結來說,跨來源請求阻擋的是…

preflight request 的目的#

  1. 相容性

<form> 是早期網站發請求的方式,但它的功能有限,<form>enctype(表示表單數據的編碼方式)只支援application/x-www-form-urlencodedmultipart/form-data,而這些符合簡單請求的定義,但<form>enctype 不支援Content-Type: application/json,而Content-Type: application/json 屬於非簡單請求。因此 <form> 僅能發出簡單請求,任何非簡單請求(例如使用 application/json)都不可能由 <form> 發出。

當新的技術(如 XMLHttpRequestfetch API)被引入時,古老網站沒有預期接收這些新型態的 request,新型態的 request 如:DELETE方法、新的 content-type application/json,但過去時代 <form><img> 等元素是唯一能發出 request 的方法,這些元素不會有上述那些新的 request 方式,為了不讓古老網站後端收到預期外 request,就先發 preflight request 確認,若後端沒針對 preflight 處理,瀏覽器不發送真正 request,早期網站不會受到傷害。

2. 安全性

防止瀏覽器在未經確認情況下,直接執行可能會對後端造成未預期影響的操作,舉例來說,用 DELETE 方法呼叫 API 時,若沒有 preflight request,瀏覽器就會直接送出 request,可能對後端造成未預期行為,為確保後端知道待會的 request 合法,才需 preflight request 確認。

跨來源請求預設不會帶上 cookie,跨來源請求要帶上 cookie,須滿足特定條件,整理如下:

跨來源的安全性問題:CORS misconfiguration#

如果跨來源非簡單請求想帶上 cookie,Access-Control-Allow-Origin 就不能是 *,需指定單一 origin,但可能有多個 origin 都要存取 API,此時就要動態調整 Access-Control-Allow-Origin 裡的 origin。動態調整的設定若有問題,可能會讓攻擊者存取到不該存取的資源。

動態調整 Access-Control-Allow-Origin 裡 origin 的錯誤示範#

錯誤示範 1:直接放入 request header 內的 origin

任何一個 origin 都能通過 CORS,程式碼如下:

app.use((req, res, next) => {
res.headers['Access-Control-Allow-Credentials'] = 'true'
res.headers['Access-Control-Allow-Origin'] = req.headers['Origin']
})

可能問題例如攻擊者可以寫一個網站 https://fake-example.com 並讓使用者點擊這網站,攻擊者在網站裡寫這段 script:

// 用 api 去拿使用者資料,並且帶上 cookie
fetch('https://api.example.com/me', {
credentials: 'include'
})
.then(res => res.text())
.then(res => {
// 成功拿到使用者資料,可以傳送到自己的 server
console.log(res)
// 把使用者導回真正的網站
window.location = 'https://example.com'
})

因為伺服器會回傳正確 header,認可 https://fake-example.com 是合格 origin,讓 fake-example 這網站也拿到 http://api.example.com/me 的資料,影響範圍視網站 API 而定,可能偷走使用者資料或使用者 token(如果有 API 可以拿 token)。

此攻擊成立的前提有幾個:

  1. CORS header 給到不該給的 origin
  2. 網站用 cookie 做身份驗證,且沒設定 SameSite(若有設定 SameSite cookie,攻擊會失效,因 cookie 帶不上去)
  3. 使用者主動點擊網站並且是登入狀態

錯誤示範 2:用 Regex 判斷 request origin 是否合法

程式碼如下:

app.use((req, res, next) => {
res.headers['Access-Control-Allow-Credentials'] = 'true'
const origin = req.headers['Origin']
// 偵測是不是 example.com 結尾
if (/example\.com$/.test(origin)) {
res.headers['Access-Control-Allow-Origin'] = origin
}
})

可過關的 origin 如:example.combuy.example.com,但fakeexample.com 也可過關,一樣會造成上面的攻擊問題。

上述這些錯誤 CORS 設置引起的漏洞稱為 CORS misconfiguration,錯誤設置的實際案例例如:

CORS 正確設置方式#

事先準備允許的 origin 清單,有在清單內的才通過,並設 sameSite cookie,程式碼如下:

const allowOrigins = [
'https://example.com',
'https://buy.example.com',
'https://social.example.com'
]
app.use((req, res, next) => {
res.headers['Access-Control-Allow-Credentials'] = 'true'
const origin = req.headers['Origin']
if (allowOrigins.includes(origin)) {
res.headers['Access-Control-Allow-Origin'] = origin
}
})

其他各種 COXX 系列 header#

其他以 CO(Cross-Origin) 開頭的 header,也和跨來源資料存取有關

在那之前,先來看看 Meltdown 與 Spectre~

嚴重的安全漏洞:Meltdown 與 Spectre#

2018 年 1 月 Google 的 Project Zeror 發布一篇 Reading privileged memory with a side-channel 文章,講了三種針對 CPU data cache 的攻擊

前兩種被稱為 Spectre,第三種稱為 Meltdown,此攻擊十分嚴重,因問題在於 CPU,不容易修復。這漏洞對後續的影響是它加速瀏覽器演進歷程、促進瀏覽器的跨來源資源存取政策。

超級簡化版 Spectre 攻擊解釋#

此為方便理解的簡化版,和原始攻擊有落差,但核心概念相似。

假設一段程式碼(C 語言)如下,這段程式碼做了幾件事:

uint8_t arr1[16] = {1, 2, 3};
uint8_t arr2[256];
unsigned int array1_size = 16;
void run(size_t x) {
if(x < array1_size) {
uint8_t y = array2[array1[x]];
}
}
size_t x = 1;
run(x);

程式碼看起來沒什麼問題,但這裡先簡單介紹一下 CPU 機制,CPU 有 Branch Prediction 和 Speculative Execution 的機制,目的是為了增進程式碼執行效率,當 CPU 執行時,若碰到 if,會先預測結果是 truefalse,若預測結果是 true,就先執行 if 內程式碼,先算出 if 內的結果,實際 if 條件執行完後,若跟預測相同則皆大歡喜,若跟預測不同,就把計算的結果丟掉。在這過程中,Branch Prediction 是「預測」分支的走向;Speculative Execution 是「基於預測結果」執行分支中的程式碼。

這個機制有什麼問題?CPU 丟棄結果後我們也拿不到啊,但問題在於它有留下線索🔺,線索就是預測執行時,運算結果會被放入 CPU cache。那要如何判斷資料是否在 CPU cache 內?可以用存取時間判斷,讀取 CPU cache 內資料較快,所謂的「Side-channel attack」就是攻擊者可利用存取時間(timing attack)來推測 CPU cache 內的資料。

再看一次上面那段程式碼,可能會有什麼問題?當我們跑多次 run(10) 後,branch prediction 預測下次也會滿足條件,提前執行 if 內程式碼,當 x 設為 200 時,預測會執行:uint8_t y = array2[array1[200]];,若 array1[200] 是 38,則執行 y = array2[38]array2[38] 被放入 CPU cache,接著實際執行發現條件不符,丟掉執行結果,此時若用 timing attack 讀取 array2 每個元素並計算時間,會發現 array2[38] 讀取時間最短,回推 array1[200] 內容是 38。

array1 長度只有 16,array1[200] 是存取到 array1 以外的東西,因此這是存取到其他不該存取到的記憶體,重複這模式可以讀取到其他地方的資料。

上述攻擊原理應用在瀏覽器,就能讀取同一 process 的其他資料,若同一個 process 有其他網站內容,就能讀取其他網站內容,這就是 Spectre 攻擊。一句話解釋就是:「在瀏覽器上,Spectre 讓你有機會讀取到其他網站的資料」。

COXX 系列 header 和 Spectre 的關係是什麼?COXX 主要目的都是為了防止一個網站能讀取到其他網站的資料,避免惡意網站跟目標網站處在同一個 process。

CORB(Cross-Origin Read Blocking)#

阻擋不合理的跨來源資源載入

Spectre 能讀取同一 process 下的資料,因此防禦方式就是不要讓其他網站資料出現在同一 process 底下。
其他網站的資料會如何出現?跨來源存取資源的方式如:

所以 CORB 機制目的就是:「如果你想讀的資料類型根本不合理,那根本不需要讀到 render process,直接把結果丟掉就好」。

讀的資料類型不合理是什麼意思?例如用 <img> 載入 MIME type 是 application/json 的 JSON 檔、用 <script> 載入 HTML。

CORB 主要保護的資料類型是 HTML、XML 跟 JSON,Chrome 會根據內容探測(sniffing)檔案類型,決定是否套用 CORB,但有誤判可能,若確定伺服器給的 content type 正確,可傳 response header X-Content-Type-Options: nosniff,Chrome 會直接用給定的 content type。

CORB 在 Chrome 已預設使用,會自動阻擋不合理的跨來源資源載入。

CORP(Cross-Origin Resource Policy)#

阻止任何跨來源資源載入,保護網站資源不被其他人載入

CORP 的前身是 From-Origin,Cross-Origin-Resource-Policy (was: From-Origin) #687 對 CORP(From-Origin) 的敘述如下:

Cross-Origin Read Blocking (CORB) automatically protects against Spectre attacks that load cross-origin, cross-type HTML, XML, and JSON resources, and is based on the browser’s ability to distinguish resource types. We think CORB is a good idea. From-Origin would offer servers an opt-in protection beyond CORB.

CORP 使用情境是知道該保護哪些資源,指定這些資源只能被哪些來源載入,例如 server 知道該保護這張圖片,所以設定這張圖片只能由哪些來源網站可載入。

CORP 可填入三種值

主流瀏覽器都已支援 CORP 的使用,可手動傳入,使用方式就是 server 回傳時設 response header Cross-Origin-Resource-Policy,程式碼如下:

app.use((req, res, next) => {
res.header('Cross-Origin-Resource-Policy', 'same-origin');
next();
});

CORP 可視為資源版的 CORS,CORS 是 API 或資料間存取的協議,讓跨來源資料存取需要許可,而 CORP 是資源(如 <img>)間的存取協議,讓任何跨來源資源載入需要許可。原先阻止跨來源資源載入方式是由 server side 自行依據 Origin 或 Referer 等值,動態決定是否回傳資料,現在就可用 CORP 來控制。不過 CORP 的「讓任何跨來源資源載入需要許可」中的任何跨來源不包含 iframe,CORP 對 iframe 無效。

CORP 阻止任何跨來源載入的目的除了安全性,還可以阻止別人載入你的資源,擁有資源者可避免支付對應流量與費用、避免 Clickjacking,也可避免隱私洩漏問題。

Site Isolation#

防止 Spectre 攻擊的方式有兩種:

如何「不讓攻擊者有機會執行 Spectre 攻擊」?Spectre 攻擊後,瀏覽器做了些調整:

如何「就算執行攻擊,也拿不到想要的資訊」?

CORB 已經提過,接下來看看 Site Isolation。

Site Isolation#

Site Isolation 會將不同網站(site)資源放在不同 process,這裡的不同網站定義和 same site 的 site 定義相同,same site 同 process,反之隔離。隔離對象是 process,隔離目的是即使有 Spectre 攻擊也讀不到其他網站的資料。Site Isolation 目前 Chrome 預設啟用,但缺點是使用的記憶體會變多,因為需要更多 process。

cross-origin isolated state#

還有一個更嚴格的隔離方式是 cross-origin isolated state,它將不同網站(origin)資源放在不同 browsing context group,這裡的不同網站和 same origin 的 origin 定義相同,same origin 同 browsing context group,反之隔離。隔離對象是 browsing context group,在設置時要確認自己網站的所有跨來源存取都合法、有權限。

使用 cross-origin isolated state 的方式是在網頁設這兩個 header:

接著先來看看 COEP(Cross-Origin-Embedder-Policy)和 COOP(Cross-Origin-Opener-Policy)是什麼吧~

COEP(Cross-Origin-Embedder-Policy)#

確保頁面上所有資源都是合法載入

COEP 可填入的值如下:

舉例來說,若想將網站 a.example.com 變成 cross-rogin isolated state,那就先幫網站加上 header Cross-Origin-Embedder-Policy: require-corp,接著在網頁引入資源 <img src="http://b.example.com/logo.jpg">,而對應提供資源的 b 傳送正確 header 如下程式碼:

app.use((req, res, next) => {
res.header('Cross-Origin-Resource-Policy', 'cross-origin');
});

這裡可看出 CORP 沒設定與設定 cross-origin 的差異,差異在於 CORP 有設 cross-origin 才能通過 COEP require-corp 要求。

COOP(Cross-Origin-Opener-Policy)#

為 same origin 加上更嚴格的 window 共享設定

COOP 目的是規範 window 跟 opener 間的關係,為何要規範? 因為用 window.open 開新網頁時,可操控新網頁 location,新網頁也可用 window.opener 操控原網頁,會有安全性問題。

COOP 可填入的值有這些:

直接用範例來解釋same-originsame-origin-allow-popups 的意思,假設網頁 A 用 window.open 開啟網頁 B,在遇到以下不同條件時,會有不同的存取限制:

由上可知,互相存取 window 的條件必須是 same-origin,但是否能真的存取,取決於 COOP header。

COOP header 的影響在於,若設定 COOP 但不符規則,window.opener 會變為 null,且無法存取 window.location(若沒設定 COOP,cross origin 也可拿到 location)。

再回到 cross-origin isolated state#

啟用 cross-origin isolated state 要設兩 header:

當啟用 cross-origin isolated state,代表頁面上所有跨來源資源你都有權限存取,因為你請求的資源,對方 server 需回應對應 header 來允許。

而進入 cross-origin isolated state 後…


最後以一個表格總結 COXX 系列。


Reference:#

如有任何問題歡迎聯絡、不吝指教✍