Files
podinfo/pkg/api/http/http.go
T
Stefan Prodan 5037beda6c Cap HTTP request and websocket message sizes
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
2026-06-08 23:58:43 +03:00

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()
}