Wiki: Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.

白話: CORS是一個(防禦)機制,允許瀏覽器向不同網域的server發送請求。CORS是一個瀏覽器技術規範,判斷要阻擋或是允許不同網域的資源存取。

browser and node.js flow

基於安全性的考量,瀏覽器預設會限制網頁做跨網域的連線。如果像是透過node.js送出請求,就不會受到限制!



Same-origin policy-同源政策

雙方必須具備相同的協定&port (if 有指定)&主機位置

A-	http://www.example.com/foo
B-	http://www.example.com/bar
C-	https://www.example.com/foo

A v.s. B >>OK!
A v.s. C >>NO!

但但…不是全面性的限制不同來源的資源存取

基本上: 同源的資源一定可以互相存取;例外: 跨來源的資源,在特定情況下才允許存取


  • cross-origin-writes 允許跨來源的寫入
    • 連結, 表單送出…
  • cross-origin embedding 允許跨來源的嵌入
    • <script src="xxxx"></script>
    • <link rel="stylesheet" href="xxxx"> 
    • <img>
    • <video>
    • <iframe>

小結: 圖片、影片、script…等資源,會被載入瀏覽器中,成為DOM的元素的,來源不管;頁面跳轉,來源不管

  • cross-origin read 允許跨來源的讀取
    • EX: 透過fetch or XHR發出的請求
    • 但….可以使用cross-origin embedding偷吃步



加映Cookies也有同源政策

Cookies是網站在使用者瀏覽網頁時,儲存在使用者端的資料

當下一次對此網站送出Request時,會一併帶上cookies

常見在紀錄購物網站記錄使用者的資訊/購物車資訊/Ad- Retargeting

在DevTools檢視cookies

cookies也有同源政策,A網站存在client端的cookie沒有辦法被B網站讀取! (localstrage & indexDB也是)

cookies


瀏覽器訊息

當網頁向外部網站要資料時,瀏覽器將Request送出,拿到Response,但瀏覽器會基於安全性考量,不讓程式(javascript)拿到Response。

範例: 利用XHR送出Request

// 透過XMLHttpRequest物件-->開啟URL-->發出請求
// Transaction完成後,XMLHttpRequest物件會包含Response和HTTP狀態等資訊
<script>
        function httpGet(theUrl) {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function () {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
                    console.log('success');
            }
            xmlHttp.open("GET", theUrl, true); // 請求初始化 (true for asynchronous)
            xmlHttp.send(null);                //送出請求
            xmlHttp.onerror = function () {
                console.log('error!!!!');
            }
        }
    </script>

會看到錯誤訊息:

CORS-error


Q:如何突破安全限制,取得跨網域的資料內容??

script可以在同網域之間互相傳送資料,但禁止不同網域之間取用方法/屬性


解法1: CORS跨源設定

Server端做調整設定,於Response Header加上Access-Control-Allow-Origin

Access-Control-Allow-Origin: *            			# 允許所有網站發送的請求
Access-Control-Allow-Origin: http://foo.example  	# 只允許 http://foo.example 的請求

瀏覽器收到Server Response,檢查Access-Control-Allow-Origin的內容,若Origin符合Access-Control-Allow-Origin,程式才能收到Response的內容

利用express提供的cors套件https://expressjs.com/en/resources/middleware/cors.html

CORS將請求分兩種: Simple Request& Preflight Request

  • Simple Request

    一般請求

  • Preflight Request (預檢請求)

    有些情況下Access-Control-Allow-Origin檢查還不夠,像是Delete/Put 這類的請求,需要取得Server的允許後,再送出真正的Request

    先送一次檢查,確認是不是能送出真正的請求

    ex: xxxx/api/posts/1 刪除文章

    preflight


小結CORS:

  1. 發出一個CORS Request,Request header中帶有Origin資源

  2. Browser檢查是simple or not-simple request

  3. simple-直接送實際的Request,browser依Response header Access-Control-Allow-Origin決定要不要給

    not-simple-通常是GET以外的Request,browser會先送一個OPTIONS Preflight Request,Browser確認Server Allow的項目,符合後才送實際的Request

    sample

CORS Lifecycle

還有其他相關的Header:

Access-Control-Allow-Headers:哪些 HTTP Header Server支持。
Access-Control-Allow-Methods:存取資源所允許的方法,用來回應預檢請求。
Access-Control-Expose-Headers:瀏覽器能夠存取伺服器回應當中哪些標頭。
Access-Control-Max-Age:預檢請求之結果可以被快取的秒數。
Access-Control-Allow-Credentials:用於驗證請求中。

實作

Server:

const express = require('express');
const cors = require('cors'); 	//express出的套件cors
const PORT = process.env.PORT || 3000;
const app = express();
app.use(cors());
app.get('/hello/:id', function (req, res, next) {
    res.json({
        msg: 'Hello world, we are CORS-enabled!'
    });
});
app.listen(PORT, () => {
    console.log(`Server listening on port ${PORT}`);
});


CORS預設為全開放:

{
  "origin": "*",
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
  "preflightContinue": false,
  "optionsSuccessStatus": 204
}


自訂:

const corsOptions = {
  origin: [
    'http://www.example.com',
    'http://localhost:8080',
  ],
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
  allowedHeaders: ['Content-Type', 'Authorization'],
};

app.use(cors(corsOptions));

p.s. Chrome 79+ no longer shows preflight CORS requests

下載 HTTP Toolkit


解法2: JSONP

JSON with Padding,有些tag src不受同源的限制,載入帶參數(querystring)的js,發送request

(type=”text/javascript”,資料藏在回傳的內容中)

當想要向http://localhost:3000/user?id=xxx取得資料,透過瀏覽器送出http://localhost:3000/user?id=1

Server根據id=1,回傳json

{
"Name":"Pon",
"id":"1",
"age":"xx"
}

或是取得資料的同時就執行script

// Client

<script>
	function jsonpCallback(res){
		// bla bla bla	
	}
</script>

<script type="text/javascript" src="/app/jsonp"></script> // JSOP
// Server

app.get('/app/jsonp', (req, res) => {
  const responseData = { data: [{ id: 1, name: 'Brian' }, { id: 2, name: 'Peter' }] }
  const responseDataText = JSON.stringify(responseData)
  res.set('Content-Type', 'application/text')
  res.send(`jsonpCallback(${responseDataText})`)
})

Client定義jsonpCallback function要實作的內容/邏輯,Server遇到/app/jsonp的請求,回傳jsonpCallback(responseDataText)

通常Server會提供一個callback,讓client可以傳入,/app/jsonp?id={id}&jsonpcallback={callback}


解法3: Proxy

  • Proxy : 在client之前,攔截/收集Request,向web server溝通,取得的資源返回給client端,Server只知道Proxy的IP位址,不知道Client的IP

    (代理client)

    forward proxy flow

  • Reverse Proxy: 在server之前,攔截/收集request,不公開真正的Server IP位址,所有請求會先給Reverse Proxy,由Reverse Proxy來分配給Server

    (代理server)

    reverse proxy flow

    Server之間不會有跨域問題,建立Proxy發送請求!

Proxy v.s. CORS


範例-Angular proxy setting

// proxy.conf.json
{
  "/api/*": {
    "target": "http://localhost:3000",		// Request target 
    "secure": false,
    "logLevel": "debug",
    "changeOrigin": true
  }
}

// 在app中所有/api/...開頭的request會被置換成http://localhost:3000/api/....送到proxy
// proxy再把請求送給後端

參考: https://juristr.com/blog/2016/11/configure-proxy-api-angular-cli/

流程圖



加映-沒有同源保護下會….

Cross site request forgery

CSRF,跨站請求偽造

在不同的domain下,偽造出使用者發出的Request,因為瀏覽器機制是發出Request給某網域時,一併帶上所屬cookie。

範例取自讓我們來談談 CSRF

// 在網站中製作一個惡意連結
<a href='https://books.com/delete?id=3'>影片播放</a> 
// 送出Request給books網站,cookies一併被送出

Server端收到請求,也驗證了session,於是執行了刪除

// 不知不覺
<img src='https://books.com/delete?id=3' width='0' height='0' />


所以…..使用者可以養成隨手登出好習慣

Server端可以檢查referer、圖形驗證、簡訊驗證碼

瀏覽器也可以來做防禦,如chrome實作SameSite,阻止跨網站發送cookies,可防止CSRF攻擊

// Server
// Set-Cookie: key=value   SameSite有三個值 None, Strict & Lax 
Set-Cookie: cat_image_loaded=1, SameSite=None 不限制同源
Set-Cookie: cat_image_loaded=1, SameSite=Strict 限同源網站,完全禁止第三方cookies
Set-Cookie: cat_image_loaded=1, SameSite=Lax 部分request允許 (<a>, <link> )



clickjacking

例如:iframe visibility調整將真實網頁隱藏,假的網頁誘使User點擊(ex:輸入帳號密碼)

另外也可以控制滑鼠點擊的任何連結,甚至控制Camera或麥克風

click jacking

取自:https://www.netsparker.com/blog/web-security/clickjacking-attacks/

解法: 如果希望整個網站都不准其他網頁內嵌,可以在web server的header中加入X-Frame-options (DENY/ SAMEORIGIN/ ALLOW-FROM uri),瀏覽器根據同源政策下,符合的網頁才能使用iframe,需要瀏覽器支援這個Header才有效果。


延伸閱讀&參考圖文

原來 CORS 沒有我想像中的簡單

Day8-什麼是CROS (Cross-Origin Resource Sharing)

同源政策 (Same-origin policy)

Same Origin Policy 同源政策 ! 一切安全的基礎

CORS, Same-origin policy 和 iFrame

Same-origin policy

讓我們來談談 CSRF

跨瀏覽器攻擊手法:Clickjacking

跨來源資源共用

同源政策與跨來源資源共用(cors)

Using a simple proxy to add CORS support to Ex Libris APIs

輕鬆理解 Ajax 與跨來源請求

Chapter 4. Handling preflight requests

何謂網路釣魚,Reverse Proxy 又是什麼