二狗子翻車了,只因上了這個網站……

語言: CN / TW / HK

今天故事的主角還是大家熟識的二狗子。二狗子拿到了一筆項目獎金,在好好犒勞了自己一頓後,決定把剩下的錢在銀行存個定期。

他用瀏覽器訪問了 www.bank.com,輸入了用户名和密碼後,成功登錄。

bank.com 返回了 cookie 用來標識二狗子這個用户。

不得不説,瀏覽器是個認真負責的工具,它會把這個 cookie 記錄下來,以後二狗子每次向 bank.com 發起 HTTP 請求,瀏覽器都會準確無誤地把 cookie 加入到 HTTP 請求頭部中,一起發送到 bank.com,這樣 bank.com 就知道二狗子已經登陸過了,就可以按照二狗子的請求來做事情,比如查看餘額、轉賬取錢。

二狗子存完錢,看着賬户餘額,心中暗喜。於是,他打開了 www.meinv.com,去看自己喜歡的電影。

但二狗子不知道的是,瀏覽器把 meinv.com 的 HTML、JavaScript 都下載到本地,開始執行。而其中某個 JavaScript 中,偷偷創建了一個 XMLHttpRequest 對象,然後向 bank.com 發起了 HTTP 請求 。

瀏覽器嚴格按照規定,把之前存儲的 cookie 也添加到 HTTP 請求中。但是 bank.com 根本不知道這個 HTTP 請求是 meinv.com 的 JavaScript 發出的,還以為是二狗子發出的。bank.com 檢查了cookie,發現這是一個登錄過的用户,於是兢兢業業地去執行請求命令,二狗子的個人信息就泄露了。(ps. 實際中實施這樣一次攻擊不會這麼簡單,銀行網站肯定是做了其他很多安全校驗的措施,本故事只是用來説明基本原理。)

可憐的二狗子還不知道發生了什麼,已經遭受了錢財損失。那我們來幫他覆盤一下為什麼會發生這種情況。

首先,每當訪問 bank.com 的時候,不管是人點擊按鈕訪問鏈接,還是通過程序的方式,存儲在瀏覽器的 bank.com 的 cookie 都會進行傳遞。

其次,從 meinv.com 下載的 JavaScript 利用 XMLHttp 訪問了 bank.com。

第一點我們是無法阻止的,如果阻止了,cookie 就喪失了它的主要作用。

對於第二點,瀏覽器必須做出限制,不能讓來自 meinv.com 的 JavaScript 去訪問 bank.com。這個限制就是同源策略。

同源策略

瀏覽器提供了 fetch API 或 XMLHttpRequest 等方式,它們可以使我們方便快捷地向後端發起請求,取得資源,展示在前端上。而通過 fetch API 或 XMLHttpRequest 等方式發起的 HTTP 請求,就必須要遵守同源策略 。 那什麼是同源策略呢?同源策略(same-origin policy)規定了當瀏覽器使用 JavaScript 發起 HTTP 請求時,如果是請求域名同源的情況下,請求不會受到限制。但如果是非同源的請求,則會強制遵守 CORS (Cross-Origin Resource Sharing,跨源資源共享) 的規範,否則瀏覽器就會將請求攔截。

那什麼情況下是同源呢?同源策略非常嚴格,要求兩個 URL 必須滿足下面三個條件才算同源:

1、協議(http/https)相同;

2、域名(domain)相同;

3、端口(port)相同。

舉個例子:下列哪些 URL 地址與 https://www.bank.com/withdraw.html 屬於同源?

因此,當我們請求不同源的 URL 地址時,就會產生一個跨域 HTTP 請求(cross-origin http request)。

例如想要在 https://www.upyun.com 的頁面上顯示來自 https://opentalk.upyun.com 的資訊內容,我們使用瀏覽器提供的 fetch API 來發起一個請求:

try {
  fetch('https://opentalk.upyun.com/data')
} catch (err) {
  console.error(err);
}

這就產生了一個跨域請求,跨域請求則必須遵守 CORS 的規範。

當請求的服務器沒有配置允許 CORS 訪問或者不允許來源地址的話,請求就會失敗,在 Chrome 的開發者工具台上就會看到以下的經典錯誤:

Access to fetch at 'https://opentalk.upyun.com/data' from origin 'https://www.upyun.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

那在實際應用中,我們該如何正確地設定 CORS 呢?

什麼是 CORS

CORS 是針對不同源(域)的請求而制定的規範。瀏覽器在請求不同域的資源時,被跨域請求的服務端必須明確地告知瀏覽器其允許何種請求。只有在服務器允許範圍內的請求才能夠被瀏覽器放行並請求,否則會被瀏覽器攔截,訪問失敗。

在 CORS 規範中,跨域請求主要分為兩種:簡單請求(simple request)和非簡單請求(not-so-simple request)。

簡單請求

簡單請求必須符合以下四個條件,實際開發中我們一般只關注前面兩個條件:

(1)使用 GET、POST、HEAD 其中一種方法;

(2)只使用瞭如下的安全請求頭部,不得人為設置其他請求頭部:

  • Accept

  • Accept-Language

  • Content-Language

Content-Type 僅限以下三種:

  • text/plain

  • multipart/form-data

  • application/x-www-form-urlencoded

(3)請求中的任意 XMLHttpRequestUpload 對象均沒有註冊任何事件監聽器,XMLHttpRequestUpload 對象可以使用 XMLHttpRequest.upload 屬性訪問;

(4)請求中沒有使用 ReadableStream 對象。

不符合以上任一條件的請求就是非簡單請求。瀏覽器對於簡單請求和非簡單請求,處理的方式也不一樣。

對於簡單請求,瀏覽器會直接發出 CORS 請求。具體來説,就是在請求頭信息中,自動地增加一個Origin (來源)字段。

Origin 的值中,包含請求協議、域名和端口三個部分,用於説明本次請求來自哪個源。服務器可以根據這個值,決定是否同意這次請求。例如下面的請求頭報文:

GET /data HTTP/2
Host: opentalk.upyun.com
accept-encoding: deflate, gzip
accept: */*
origin: https://www.upyun.com
......

如果 Origin 指定的源不在服務器允許範圍內,服務器會返回響應一個正常的 HTTP,瀏覽器發現迴應頭部中,如果沒有包含 Access-Control-Allow-Origin 字段,就會拋出錯誤。需要注意的是,這種錯誤無法通過狀態碼識別,HTTP 響應的狀態碼有可能是 200。

如果 Origin 指定的源在允許範圍內的話,響應頭部中,就會有以下幾個字段:

Access-Control-Allow-Origin: https://www.upyun.com
Access-Control-Allow-Headers: Authorization
Access-Control-Expose-Headers: X-Date
Access-Control-Allow-Credentials: true

大家可能也看出來了一個特點,與 CORS 請求相關的字段,都以 Access-Control- 開頭。

如果跨域請求是被允許的,那麼響應頭部中是必須有 Access-Control-Allow-Origin 頭部的。它的值要麼是請求時 Origin 字段的值,要麼是一個 *,表示接受任意域名的請求。

Access-Control-Allow-Credentials 是一個可選字段,它的值是一個布爾值,表示是否允許發送Cookie。如果發起跨域請求時,設置了 withCredentials 標誌為 true,瀏覽器在發起跨域請求時,也會同時向服務器發送 cookie。如果服務器端的響應中不存在 Access-Control-Allow-Credentials 頭部,瀏覽器就不會響應內容。

特別需要説明的是,如果請求端設置了 withCredentials ,Access-Control-Allow-Origin 的值就必須是具體的域名值,而不能設置為 *,否則瀏覽器也會拋出跨域錯誤。

Access-Control-Expose-Headers 也是一個可選頭部。當進行跨域請求時,XMLHttpRequest 對象的 getResponseHeader()方法只能拿到 6 個基本響應字段:

  • Cache-Control

  • Content-Language

  • Content-Type

  • Expires

  • Last-Modified

  • Pragma

而如果開發者需要獲取其他響應頭部字段,或者一些自定義響應頭部,服務器就可以通過設置 Access-Control-Expose-Headers 頭部來指定發起端可訪問的響應頭部。

非簡單請求

非簡單請求往往是對服務器有特殊要求的請求,比如請求方法為 PUT 或 DELETE,或者 Content-Type 字段類型是 application/json。

對於非簡單請求的 CORS 請求,瀏覽器會在正式發起跨域請求之前,增加一次 HTTP 查詢請求,我們稱為預檢請求(preflight)。瀏覽器會先詢問服務器,當前的域名是否在服務器的許可名單之中,以及可以使用哪些 HTTP 請求方法和請求頭部字段。只有得到肯定答覆,瀏覽器才會發出正式的跨域請求,否則就會報錯。

比方説我們使用代碼發起一個跨域請求:

fetch('http://opentalk.upyun.com/data/', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-CUSTOM-HEADER': '123'
  }
})

瀏覽器會發現這是一個非簡單請求,它會自動發送一個 OPTIONS 的預檢請求,其中核心內容有兩部分,Access-Control-Request-Method 表示後面的跨域請求需要用到的方法,Access-Control-Request-Headers 表示後面的跨域請求頭內會有該內容。

OPTIONS /data/ HTTP/1.1
Host: opentalk.upyun.com
Origin: http://www.upyun.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-MY-CUSTOM-HEADER, Content-Type

服務器收到預檢請求後,檢查這些特殊的請求方法和頭自己能否接受,如果接受,會在響應頭部中包含如下信息:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: X-Date, range, X-Custom-Header, Content-Type
Access-Control-Expose-Headers: X-Date, X-File, Content-type
......

上面的 HTTP 響應中,關鍵的是 Access-Control-Allow-Origin 字段,* 表示同意任意跨源請求都可以請求數據。部分字段我們在簡單請求中解釋過了,這裏挑幾個需要注意的頭部解釋一下。

  • Access-Control-Allow-Methods,這是個不可缺少的字段,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。

  • Access-Control-Allow-Headers 字段為一個逗號分隔的字符串,表明服務器支持的所有請求頭部信息字段,不限於瀏覽器在預檢中請求的字段。

  • Access-Control-Max-Age:該字段可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是 1 天(86400 秒),在此期間,不用再發出另一條預檢請求。

又拍雲 CORS 配置

以上就是對 CORS 的一個簡單介紹。如果您使用了又拍雲的 CDN 或者雲存儲服務, 在訪問中遇到跨域問題,是可以非常快速便捷的進行 CORS 配置的。

登陸服務控制枱,依次進入:服務管理 > 功能配置 > 訪問控制 > CORS 跨域共享,點擊【管理】按鈕即可開始配置。如下圖所示:

相信您看完本篇文章,對配置界面的各個字段都不再陌生啦。

推薦閲讀

網絡安全(一):常見的網絡威脅及防範

【白話科普】從“熊貓燒香”聊聊計算機病毒