自制 ResponseWriter:Go 安全 HTTP
Go 的 http.ResponseWriter 會直接向套接字(socket)寫入數據,這可能會導致一些隱蔽的 bug,例如忘記設置狀態碼,或是在爲時已晚的時候意外修改了響應頭(header)。
本文將展示如何通過包裝 ResponseWriter 來強制執行自定義規則,例如要求 WriteHeader() 以及在出錯後阻止寫入操作,從而讓你的處理器(handler)更安全、也更易於梳理邏輯。
我用 Go 寫過成百上千個 HTTP 處理器,卻一直在不經意間犯着同一個隱蔽的錯誤。直到參加了 empijei[1] 的 安全代碼研討會 [2],我才恍然大悟:http.ResponseWriter 默認情況下是不安全的,但它的設計初衷是作爲基礎,供你構建自己的自定義邏輯。
從研討會中我學到的一些要點是:
http.ResponseWriter是一個接口(interface)- 你可以通過包裝另一個
ResponseWriter來實現你自己的ResponseWriter,從而強制執行某些規則
我寫的每個處理器,或多或少都是這樣開頭的:
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("helo world"))
})
http.ListenAndServe(":8080", nil)
值得注意的一點是,由於我沒有顯式調用,w.Write 調用也會替我調用 w.WriteHeader:
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("helo world"))
})
w.WriteHeader 會寫入響應的狀態碼,以及存儲在 w.Header() map 中的所有響應頭條目。
如果我想強制我所有的處理器都顯式設置狀態碼,即使它是 200 OK,以便我能確保自己沒有忘記設置,那該怎麼做呢?
此外,設置新的響應頭或更改現有的響應頭將不會產生任何效果,並且你也不會收到任何類型的錯誤提示:
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
// WARN: the following line silently has no effects
w.Header().Set("content-type", "application/json")
w.Write([]byte("helo world"))
})
如果我們至少能收到一個警告,提示我們可能不小心做錯了什麼,那不是很好嗎?
我們再來看另一個例子:
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
response, err := database.LoadResponse()
if err != nil {
log.Println("error loading response:", err)
w.WriteHeader(500)
w.Write([]byte("error :("))
}
w.Write([]byte("response: "))
w.Write(response)
})
上面的代碼中有一個 bug,我在過去不止一次犯過這個錯誤。
你能發現它嗎?
答案在……
3……
2……
1……
問題在於 if 條件判斷後缺少了一個提前返回(early return)。這意味着即使數據庫返回了錯誤,處理器中用於打印響應的其餘代碼仍會繼續執行!
很好。那麼我們能做些什麼呢?
問題之一在於,我們正在使用的 http.ResponseWriter 是一個實際的寫入器(writer),它會直接向底層的 TCP 套接字寫入數據,而不是在寫入之前_完全準備好_整個響應。
http.ResponseWriter 是一個接口。讓我們自己來實現它,並強制執行我們的自定義規則:
type HttpWriter struct {
w http.ResponseWriter // wrap an existing writer
}
func NewHttpWriter(w http.ResponseWriter) http.ResponseWriter {
return &HttpWriter{
w: w,
}
}
我們只需要實現少數幾個方法:
// implement http.ResponseWriter
func (w *HttpWriter) Header() http.Header {
return w.w.Header()
}
func (w *HttpWriter) Write(data []byte) (int, error) {
return w.w.Write(data)
}
func (w *HttpWriter) WriteHeader(statusCode int) {
w.w.WriteHeader(statusCode)
}
// it's actually a good idea to implement a
// Flusher version for our writer as well
type HttpWriterFlusher struct {
*HttpWriter // wrap our "normal" writer
http.Flusher // keep a ref to the wrapped Flusher
}
func (w *HttpWriterFlusher) Flush() {
w.Flusher.Flush()
}
// modify the constructor to either return HttpWriter or
// HttpWriterFlusher depending on the writer being wrapped
func NewHttpWriter(w http.ResponseWriter) http.ResponseWriter {
httpWriter := &HttpWriter{
w: w,
}
if flusher, ok := w.(http.Flusher); ok {
return &HttpWriterFlusher{
HttpWriter: httpWriter,
Flusher: flusher,
}
}
return httpWriter
}
讓我們把這一切串聯起來。
通過編寫一箇中間件(middleware),你就可以輕鬆地開始使用 HttpWriter 來替換掉之前使用的任何默認 http.ResponseWriter:
func middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := NewHttpWriter(w)
h.ServeHTTP(writer, r)
})
}
現在,是時候通過自定義 HttpWriter 的實現來找點樂子了。
示例 1:想在每次調用 Write() 時,如果沒有 WriteHeader() 就打印一條警告日誌嗎?完全可以!
type HttpWriter struct {
w http.ResponseWriter // wrap an existing writer
headerWritten bool
}
func (w *HttpWriter) Write(data []byte) (int, error) {
if !w.headerWritten {
log.Println("warn: invoked Write() without WriteHeader(statusCode)")
}
return w.w.Write(data)
}
func (w *HttpWriter) WriteHeader(statusCode int) {
w.w.WriteHeader(statusCode)
w.headerWritten = true
}
示例 2:如果狀態碼已設置爲 500,想避免寫入任何內容嗎?
type HttpWriter struct {
w http.ResponseWriter // wrap an existing writer
statusCode int
}
func (w *HttpWriter) Write(data []byte) (int, error) {
if w.statusCode >= 500 {
log.Println("warn: ignoring Write(), status code is 500")
return 0, nil
}
return w.w.Write(data)
}
func (w *HttpWriter) WriteHeader(statusCode int) {
w.w.WriteHeader(statusCode)
w.statusCode = statusCode
}
這完全取決於你!你可以隨心所欲地調整你的規則。http.ResponseWriter 任你改造。
感謝 empijei[1] 和 loresuso[3] 閱讀了本文的初稿 ❤️
引用鏈接
- https://empijei.science/
- https://golab.io/talks/a-simple-approach-to-secure-code
- https://github.com/loresuso
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pcbKq62b0qZf9ZyqHCY9Tw