mirror of
https://github.com/stefanprodan/podinfo.git
synced 2026-08-20 20:56:29 +00:00
Unauthenticated clients could exhaust process memory (and, for /store, disk) by posting arbitrarily large bodies, and could OOM or pin goroutines via the /ws/echo websocket. - Wrap request bodies in http.MaxBytesReader (10 MiB) on the echo, store, cache and token handlers via a shared readLimitedBody helper - Bound /ws/echo: per-message read limit, idle read deadline with ping/pong keepalive, and write deadlines - Add regression tests for both limits
128 lines
3.9 KiB
Go
128 lines
3.9 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/stefanprodan/podinfo/pkg/version"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// maxRequestBodySize caps how much of a request body the body-reading handlers
|
|
// buffer into memory. Without this bound an unauthenticated client can POST an
|
|
// arbitrarily large body and exhaust process memory (and, for /store, disk).
|
|
const maxRequestBodySize = 10 << 20 // 10 MiB
|
|
|
|
func randomErrorMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
rand.Seed(time.Now().Unix())
|
|
if rand.Int31n(3) == 0 {
|
|
|
|
errors := []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusConflict}
|
|
w.WriteHeader(errors[rand.Intn(len(errors))])
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func versionMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
r.Header.Set("X-API-Version", version.VERSION)
|
|
r.Header.Set("X-API-Revision", version.REVISION)
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (s *Server) JSONResponse(w http.ResponseWriter, r *http.Request, result interface{}) {
|
|
body, err := json.Marshal(result)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
s.logger.Error("JSON marshal failed", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(prettyJSON(body))
|
|
}
|
|
|
|
func (s *Server) JSONResponseCode(w http.ResponseWriter, r *http.Request, result interface{}, responseCode int) {
|
|
body, err := json.Marshal(result)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
s.logger.Error("JSON marshal failed", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.WriteHeader(responseCode)
|
|
w.Write(prettyJSON(body))
|
|
}
|
|
|
|
func (s *Server) ErrorResponse(w http.ResponseWriter, r *http.Request, span trace.Span, error string, code int) {
|
|
data := struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}{
|
|
Code: code,
|
|
Message: error,
|
|
}
|
|
|
|
span.SetStatus(codes.Error, error)
|
|
|
|
body, err := json.Marshal(data)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
s.logger.Error("JSON marshal failed", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(prettyJSON(body))
|
|
}
|
|
|
|
// readLimitedBody reads the request body up to maxRequestBodySize. It returns
|
|
// the body and true on success. On an oversized body it writes a 413 response,
|
|
// on any other read error a 400, and returns false so the caller returns early.
|
|
func (s *Server) readLimitedBody(w http.ResponseWriter, r *http.Request, span trace.Span) ([]byte, bool) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
var maxErr *http.MaxBytesError
|
|
if errors.As(err, &maxErr) {
|
|
s.ErrorResponse(w, r, span, "request body too large", http.StatusRequestEntityTooLarge)
|
|
return nil, false
|
|
}
|
|
s.logger.Error("reading the request body failed", zap.Error(err))
|
|
s.ErrorResponse(w, r, span, "invalid request body", http.StatusBadRequest)
|
|
return nil, false
|
|
}
|
|
return body, true
|
|
}
|
|
|
|
// setRawResponseHeaders prevents XSS by ensuring browsers never interpret raw responses as HTML.
|
|
func setRawResponseHeaders(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("Content-Security-Policy", "default-src 'none'")
|
|
}
|
|
|
|
func prettyJSON(b []byte) []byte {
|
|
var out bytes.Buffer
|
|
json.Indent(&out, b, "", " ")
|
|
return out.Bytes()
|
|
}
|