自制 ResponseWriter:Go 安全 HTTP

Go 的 http.ResponseWriter 會直接向套接字(socket)寫入數據,這可能會導致一些隱蔽的 bug,例如忘記設置狀態碼,或是在爲時已晚的時候意外修改了響應頭(header)。

本文將展示如何通過包裝 ResponseWriter 來強制執行自定義規則,例如要求 WriteHeader() 以及在出錯後阻止寫入操作,從而讓你的處理器(handler)更安全、也更易於梳理邏輯。

我用 Go 寫過成百上千個 HTTP 處理器,卻一直在不經意間犯着同一個隱蔽的錯誤。直到參加了 empijei[1] 的 安全代碼研討會 [2],我才恍然大悟:http.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] 閱讀了本文的初稿 ❤️

引用鏈接

  1. https://empijei.science/
  2. https://golab.io/talks/a-simple-approach-to-secure-code
  3. https://github.com/loresuso
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pcbKq62b0qZf9ZyqHCY9Tw