.NET Core 的 http Host 標頭防護與 nginx

因為家裡有丟一些測試用的網站(Web),所以常常用到 nginx 做轉發避免太多測試用的 VM 搶光有限的 IP ,方法如下:透過 nginx 解析 http 請求 (Request) 中的 Host 標頭 (header) 後,再 Host 標頭轉發到不同的 VM。因為有借朋友放 .NET Core ,他在測試 SingnalR 有發生一些問題,以下紀錄災難發生與修復的過程。

原本的 nginx 設定檔大概長這個樣子:

server {
  include config.pool/listen.conf;
  server_name dotnet-test.harekaze.moe;

  access_log /var/log/nginx/access_dotnet-test-harekaze-moe.site.log;
  error_log /var/log/nginx/error_dotnet-test-harekaze-moe.site.log;

  # SSL 跳過,反正都是用 nginx-auto-config 產生的

  location / {
    proxy_pass http://192.168.88.70:8080;
    include proxy_params;
  }
  
}

SignalR 使用 WebSocket 做傳輸,按照設定了多次 WebSocket 搭配 nginx 設定的經驗,因為 nginx 預設會覆寫部分瀏覽器的請求標頭,再轉發過去 proxy_pass 的目標導致 WebSocket 通道建立失敗,所以參考了 Socket.io 的設定檔貼上,大概長這樣:

server {
  # ...

  location / {
    proxy_pass http://192.168.88.70:8080;
    include proxy_params;
  }

  location /socket {
    proxy_pass http://192.168.88.70:8080;
    include proxy_params;
    # ref: https://socket.io/docs/using-multiple-nodes/#NginX-configuration
    proxy_set_header Host $host;
    # enable WebSockets
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

不過這時候奇怪的事情就發生了,本來通道建立失敗的 /socket 路徑,現在直接變成 400 ,連握手的過程都不見了,如下:

400 Bad Request

首先當然是追查 log ,但是 error.log 裡面沒東西,所以肯定不是 nginx 回應 400 Bad Request,而是轉到 .NET Core 網站服務後,才返回 400 Bad Request。一行一行的取消註解後,最後發現必須這樣設定才能讓 Socket 正常握手

server {
  # ...

  location /socket {
    proxy_pass http://192.168.88.70:8080;
    include proxy_params;
    # ref: https://socket.io/docs/using-multiple-nodes/#NginX-configuration
    # proxy_set_header Host $host;
    # enable WebSockets
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

或是這樣

server {
  # ...

  location /socket {
    proxy_pass http://192.168.88.70:8080;
    # include proxy_params;
    # ref: https://socket.io/docs/using-multiple-nodes/#NginX-configuration
    proxy_set_header Host $host;
    # enable WebSockets
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

Host 這個標頭為什麼會造成讓 .NET Core 的 http 服務認為這是不正常的請求呢?回頭去看看 proxy_params 裡面寫了什麼

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

根據 nginx 的論壇上的討論, $http_host 會有主機名稱(Host)與連接埠號(Port)資訊 ,而 $host 只會有主機名稱。顯然我們透過 proxy_set_header 設定了兩次 Host 這個標頭(header)。那究竟是什麼樣的 Host 標頭會讓 .NET Core 直接回應 400 錯誤呢? curl 是重現特殊 Request 很方便的工具,透過 Chrome 開發者工具複製 curl 的 bash 指令並精簡後如下:

curl 'http://192.168.88.70:8080/' \
  -H 'Connection: keep-alive' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \
  -H 'Accept-Encoding: gzip, deflate' --compressed

但是 nginx 的文件也沒說用兩次 proxy_set_header 會發生什麼事,那只好猜猜看了:

  • 會出現兩個 Host 標頭
  • 只會有一個 Host 標頭,但是有兩串資訊

如果是第一種狀況, curl 指令如下:

curl 'http://192.168.88.70:8080/' \
  -H 'Host: dotnet-test.harekaze.moe:80'
  -H 'Host: dotnet-test.harekaze.moe'
  -H 'Connection: keep-alive' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \
  -H 'Accept-Encoding: gzip, deflate' --compressed

不過 .NET Core 正確回應網頁內容出來了,那試試看兩串資訊被塞進同一個 Host 標頭,根據順序應該會變成 $http_host$host ,所以 curl 指令如下:

curl 'http://192.168.88.70:8080/' \
  -H 'Host: dotnet-test.harekaze.moe:80dotnet-test.harekaze.moe'
  -H 'Connection: keep-alive' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \
  -H 'Accept-Encoding: gzip, deflate' --compressed

接著 .NET Core 就再也沒有把 html 內容吐出來了,加上 -v 也看到 http 狀態是 400 ,結案收工。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料