A10: 伺服端請求偽造(SSRF, Server-Side Request Forgery)
漏洞概述
伺服端請求偽造(SSRF)發生在應用程式允許用戶提供 URL,並讓伺服器代為請求該資源,但未進行適當的驗證與過濾。攻擊者可利用這個漏洞讓伺服器發送惡意請求,例如:
- 存取內部系統(如
http://localhost/admin或http://192.168.1.1:8080) - 讀取內部服務(如 Redis、ElasticSearch、AWS metadata)
- 對內部 API 進行未授權存取
- 進行端口掃描(透過 HTTP 回應時間來判斷服務是否開放)
- 利用伺服器發送惡意請求攻擊其他網站(DDoS 放大攻擊)
這類攻擊特別危險,因為攻擊者可以透過伺服器執行請求,繞過防火牆,攻擊內部網路。
問題分析
1. 允許用戶提供 URL,但未過濾內部請求
許多應用程式允許用戶輸入 URL 來獲取遠端資源,例如預覽網站縮圖或檢索 API 資料。如果應用程式未對 URL 進行適當的過濾,攻擊者可透過這個機制請求內部系統。
❌ 攻擊方式
curl -X POST -d 'url=http://localhost:8080/admin' http://example.com/fetch
- 攻擊者透過
/fetchAPI 請求 內部管理介面 - 可能獲取敏感資訊,如伺服器日誌、資料庫狀態等
❌ 示範程式碼
package main
import (
"fmt"
"io"
"net/http"
)
func fetchHandler(w http.ResponseWriter, r *http.Request) {
url := r.FormValue("url") // 使用者提供 URL
resp, err := http.Get(url) // 直接請求
if err != nil {
http.Error(w, "請求失敗", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body) // 回傳內容給用戶
}
func main() {
http.HandleFunc("/fetch", fetchHandler)
http.ListenAndServe(":8080", nil)
}
❌ 問題
- 未檢查 URL,導致攻擊者可請求內部服務,如
http://localhost:8080/admin。 - 可存取 AWS Metadata(
http://169.254.169.254/latest/meta-data/),竊取 AWS 金鑰。 - 可用來 掃描內部網路(判斷哪些端口開放)。
✅ 修補措施
- 限制請求範圍(只能請求特定網域)
- 禁止存取
localhost、內部 IP、雲端 Metadata 伺服器 - 使用 DNS 解析,避免 IP 欺騙
- 使用 allowlist(白名單),只允許特定的 API 網域
🛠️ 修正後(過濾內部請求)
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// 限制允許的網域
var allowlist = []string{"example.com", "api.example.com"}
func isValidURL(targetURL string) bool {
parsedURL, err := url.Parse(targetURL)
if err != nil {
return false
}
// 阻擋內部 IP
if strings.HasPrefix(parsedURL.Host, "localhost") || strings.HasPrefix(parsedURL.Host, "127.") || strings.HasPrefix(parsedURL.Host, "169.254.") {
return false
}
// 只允許特定網域
for _, domain := range allowlist {
if strings.HasSuffix(parsedURL.Host, domain) {
return true
}
}
return false
}
func fetchHandler(w http.ResponseWriter, r *http.Request) {
targetURL := r.FormValue("url")
if !isValidURL(targetURL) {
http.Error(w, "非法請求", http.StatusForbidden)
return
}
resp, err := http.Get(targetURL)
if err != nil {
http.Error(w, "請求失敗", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/fetch", fetchHandler)
http.ListenAndServe(":8080", nil)
}
🛠️ 修正點:
- 限制請求範圍(使用 allowlist)
- 阻擋內部 IP(
localhost、127.0.0.1、169.254.169.254) - 防止攻擊者繞過驗證(禁止
@、雙重解析)