
這章要學會什麼
- 寫一個統一的「呼叫服務」,讓所有對上游的呼叫都走同一條路:呼叫端只給「版本編號+參數」,網址、方法、標頭、憑證全部由已核准的版本內容決定。
- 在送出請求之前,依序檢查版本已發布、參數符合核准的規格、網址在允許清單內,任何一項不通過都不送出請求。
- 認識「伺服器端請求偽造」這種攻擊手法,並知道怎麼在連線的那一刻擋住它。
- 把上游各種失敗(逾時、連不上、錯誤狀態碼、轉址)轉換成固定的錯誤代碼,每次呼叫都留下一筆可追查的紀錄。
- 知道為什麼「新增資料」類的請求絕不自動重試,以及「防重複鍵」怎麼保證同一把鍵重送也不會建立第二筆資料。
先備知識
要先完成第 7 章。要知道基本的網路請求跟「取消」的概念,以及 HTTP 的 3xx 狀態碼代表「轉址」。
觀念白話講
為什麼需要一道統一的「安全閘道」
平台要代替會員(跟他們的 AI)去呼叫別人的服務。如果每個地方各自發送網路請求,會發生:逾時設定不一致、錯誤各自處理、密鑰散落在程式各個角落、呼叫紀錄漏寫。更危險的是:只要有任何一處讓呼叫端能夠影響到真正打出去的網址或標頭,平台就變成攻擊者的跳板。所以這門課只設計一個地方會真正對上游發送 HTTP 請求,後面章節不管是測試工具、AI 工具呼叫,都經過同一道閘道。
呼叫端能決定什麼、不能決定什麼
| 項目 | 由誰決定 | 理由 |
|---|---|---|
| 要呼叫哪個版本 | 呼叫端(給版本編號) | 使用者選擇要用的工具 |
| 參數值 | 呼叫端,但必須符合這個版本核准過的規格 | 參數是業務內容;規格是審核過的東西 |
| 網址、呼叫方法 | 已核准的版本內容 | 呼叫端能改網址,等於能叫平台打任何地方 |
| 標頭(含密鑰) | 安全閘道 | 呼叫端能加標頭,就能偽造身分或覆蓋密鑰 |
| 逾時、回應大小上限 | 閘道的固定設定 | 避免一支慢服務或巨大回應拖垮整個平台 |
呼叫端要送的「請求」這個資料格式裡,根本沒有網址跟標頭這兩個欄位可以填——就算測試工具的表單刻意多塞這兩項資料,也不會有任何程式去讀取它們。
什麼是「伺服器端請求偽造」(SSRF)
這是指攻擊者想辦法讓伺服器「代替他」發出請求,去存取攻擊者原本自己連不到的地方——例如雲端主機的內部憑證服務、只綁在本機的管理介面、或公司內網。攻擊的入口就在「登記一支服務」這個動作:會員填的網址會被平台呼叫。這門課設計了四層防線:
- 審核:管理員看得到端點網址,可以退回,但人總會有看漏的時候。
- 精確允許清單:只有伺服器設定檔裡登記過、逐字相同的網址才准呼叫,會員無法透過任何頁面擴充這份清單。
- 連線當下再檢查一次 IP:就算網址在允許清單內,如果它實際解析出來的位址是私有網段、本機位址,一樣拒絕連線——因為網域名稱可以「先騙過檢查,連線那一刻才偷偷改指向內網」,所以檢查必須發生在「真正要連線」的那一刻,而不是提早查一次就算數。
- 不跟隨轉址:上游服務如果回傳「轉址到某個網址」,平台一律不會跟過去。
逾時、取消,跟「絕不自動重試」
每次呼叫都有固定的逾時秒數,而且逾時判斷跟「使用者自己放棄、關掉頁面」是分開處理的兩種情況。「新增資料」類的請求(例如建立工單)絕對不會自動重試——因為平台無法確定「第一次真的失敗」還是「上游其實已經處理完成,只是回應在半路遺失」,自動重送有可能造成同一件事被建立兩次。所以每一個新增資料的請求都會附上一把「防重複鍵」,上游服務可以據此判斷「這是不是同一個請求的重新傳送」,重複送出多次也只會產生一筆結果。
把各種失敗,都轉成固定的錯誤代碼
不管是「參數格式不對」「網址不在允許清單」「連線目標是私有位址」「上游逾時」「上游回錯誤」「上游要轉址」,都被轉換成一組固定不變的英文錯誤代碼,方便程式判斷;同時附上一句中文說明給人看。每一次呼叫,不管成功還是被擋下,都會產生一筆呼叫紀錄,附上一組可追查的代碼——但紀錄裡不會保存參數與回應內容本身,只記結果分類、狀態碼、耗時這類中繼資訊。
老師示範做了什麼
示範情境:在後臺「API 測試台」依序測試十幾種情況,證明「成功、參數錯誤、上游逾時」都能得到可以辨識的結果,而且呼叫者無法指定任意網址或標頭;最後到資料庫檢視每次呼叫留下的紀錄。
老師依序測試了成功、參數型別錯誤、參數不在允許值內、寫入請求重複送出同一把防重複鍵(得到相同結果)、寫入請求少帶必要旗標、上游逾時、上游轉址、雲端內部憑證位址、網域解析到本機位址、不在允許清單內的網址、偷塞網址跟標頭欄位(完全不影響結果)、已下架版本、上游服務沒開機,總共十幾種情況,每一種都得到可以清楚辨識、彼此不會搞混的結果。最後到資料庫查詢最近幾筆呼叫紀錄,證明被擋下的呼叫也一樣有紀錄,而且紀錄裡沒有任何參數或回應內容的欄位。
自己動手的步驟
步驟一:組出真正要送出的請求
依版本的網址範本跟呼叫方法,把呼叫端給的參數填進正確的位置——網址裡的路徑參數、查詢字串、或請求本文,视方法而定。特別注意:路徑參數的值一定要做網址編碼,確保像 ../ 這種內容不能被用來跳脫出原本的路徑結構。標頭字典也只在這一步組出來,包含固定的接受格式、追查代碼、給新增資料請求用的防重複鍵,以及需要的話才在這一刻才解密密鑰、填進標頭。
步驟二:允許清單與連線當下的 IP 檢查
允許清單放在伺服器的設定檔裡,是一份寫死的精確網址清單——不管會員登記什麼服務、審核通不通過,都不可能讓這份清單自己多出一項。連線當下的 IP 檢查則是在「真正建立網路連線」的那一刻,自己先查一次網域名稱對應到的位址,如果不是明確寫成「本機」名稱,卻解析到私有或本機網段的位址,就直接拒絕連線;轉址也設定成完全不跟隨。
步驟三:送出、逾時與結果分類
建立一個跟呼叫端的取消動作、以及固定秒數逾時綁在一起的取消機制,兩者任一個觸發都會中止請求。送出之後只送一次,不自動重試;依照收到的狀態碼或發生的例外,把結果分類成固定的幾種:成功、上游要轉址(視為被拒絕)、上游逾時、連不上上游、上游回傳錯誤狀態碼。不管走到哪一種結果,最後都要經過同一個地方寫紀錄,確保沒有任何一條路徑會漏寫。
步驟四:測試台與防重複鍵驗證
後臺的測試台頁面只給管理員使用,表單只讀「版本、參數、防重複鍵」三個欄位交給呼叫服務。驗證方式:選擇「建立工單」這支寫入型服務,帶同一把防重複鍵連續送出兩次,第一次會得到「已建立」的狀態碼跟一組編號,第二次應該得到相同的狀態碼與完全相同的編號——證明上游服務真的把這把鍵當成「同一件事」處理。
常見錯誤
| 症狀 | 原因 | 怎麼處理 |
|---|---|---|
| 測試台一直顯示連不上上游 | 示範用的上游服務沒有啟動,或還在跑舊版本、缺少測試用的端點 | 另開一個終端機視窗啟動最新版的示範服務 |
| 允許清單裡明明有這個網址,卻被判定為私有位址 | 這個網址雖然名字寫著別的名稱,實際解析出來卻是本機位址,而它並沒有被明確寫成「localhost」這個保留名稱 | 這正是連線當下 IP 檢查在正常運作;本機開發直接用保留名稱即可 |
| 自動化測試腳本送出表單後,卻意外把自己登出了 | 腳本的元素選取條件太寬鬆,先選到了導覽列上的登出按鈕 | 把選取條件寫得更精確,明確指定要送出表單所在的區塊 |
怎麼驗收+反向驗證
- 成功有可辨識的結果:正常呼叫得到成功、狀態碼 200、有追查代碼跟回應內容。
- 參數錯誤有可辨識的結果:型別不對的參數得到明確的「參數不符合規格」錯誤,而且耗時是 0 毫秒——代表上游根本沒有收到這個請求。
- 上游逾時有可辨識的結果:刻意呼叫一個會拖很久的測試端點,得到「逾時」分類,耗時約等於設定的逾時秒數。
- 呼叫者不能指定任意網址或標頭:刻意在請求裡夾帶額外的網址、標頭欄位,結果必須跟正常呼叫完全相同;對不在允許清單內的網址,必須得到「不允許」且沒有送出任何請求。
- 反向驗證:允許清單內、但解析到私有位址的網域仍然要被擋下;上游回傳轉址時不能被跟隨。