mirror of
https://github.com/axllent/mailpit.git
synced 2026-06-27 14:36:07 +00:00
Security: Extend request body size cap to all JSON API endpoints (GHSA-28pq-6qxg-wg5r)
The fix for GHSA-fpxj-m5q8-fphw only capped POST /api/v1/send. Four sibling endpoints (SetReadStatus, DeleteMessages, SetMessageTags, ReleaseMessage) decoded json.NewDecoder(r.Body) with no size limit, allowing an unauthenticated attacker to drive unbounded memory growth via a large IDs array. Apply a 5 MB cap in middleWareFunc so all current and future API handlers inherit it automatically. POST /api/v1/send is exempt via a bodyLimitKey context value set in sendAPIAuthMiddleware, preserving its existing config.MaxMessageSize (default 50 MB) limit. Also fix TestAPIv1SendMaxMessageSize, which was broken by a Go 1.26 change: json.Decoder now wraps reader errors in *json.SyntaxError rather than returning *http.MaxBytesError directly, causing the errors.As check to miss it and return 400 instead of 413. Reading the body with io.ReadAll before decoding surfaces the raw error, restoring correct 413 behaviour on Go 1.25 and 1.26.
This commit is contained in:
@@ -47,11 +47,11 @@ func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxMessageSize)*1024*1024)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
||||
data := sendMessageParams{}
|
||||
|
||||
if err := decoder.Decode(&data.Body); err != nil {
|
||||
// Read body before decoding so that MaxBytesReader errors are returned directly.
|
||||
// In Go 1.26+, json.Decoder wraps reader errors in *json.SyntaxError, which
|
||||
// prevents errors.As from finding *http.MaxBytesError to return a 413.
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
@@ -60,6 +60,13 @@ func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data := sendMessageParams{}
|
||||
|
||||
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&data.Body); err != nil {
|
||||
httpJSONError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var httpAuthUser *string
|
||||
if user, _, ok := r.BasicAuth(); ok {
|
||||
httpAuthUser = &user
|
||||
|
||||
@@ -41,7 +41,14 @@ var (
|
||||
// auth.UICredentials pointer (which is a data race under concurrent load).
|
||||
type contextKey int
|
||||
|
||||
const skipUIAuthKey contextKey = iota
|
||||
const (
|
||||
skipUIAuthKey contextKey = iota
|
||||
// bodyLimitKey carries an optional request body size cap (in bytes) through the
|
||||
// context. middleWareFunc reads it and applies it instead of the default 5 MB cap.
|
||||
// A value of 0 means unlimited. Used by sendAPIAuthMiddleware to honour
|
||||
// config.MaxMessageSize for the send endpoint.
|
||||
bodyLimitKey
|
||||
)
|
||||
|
||||
// Listen will start the httpd
|
||||
func Listen() {
|
||||
@@ -232,6 +239,10 @@ func basicAuthResponse(w http.ResponseWriter) {
|
||||
// auth.UICredentials pointer, which would be a data race under concurrent load.
|
||||
func sendAPIAuthMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Override the default 5 MB body cap with the send-specific limit so that
|
||||
// middleWareFunc applies config.MaxMessageSize (0 = unlimited) instead.
|
||||
r = r.WithContext(context.WithValue(r.Context(), bodyLimitKey, int64(config.MaxMessageSize)*1024*1024))
|
||||
|
||||
// If send API auth accept any is enabled, bypass all authentication.
|
||||
if config.SendAPIAuthAcceptAny {
|
||||
ctx := context.WithValue(r.Context(), skipUIAuthKey, true)
|
||||
@@ -277,6 +288,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
// and gzip compression.
|
||||
func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Limit request body size to 5 MB to prevent memory-exhaustion DoS via large
|
||||
// JSON bodies. sendAPIAuthMiddleware sets bodyLimitKey in the context to signal
|
||||
// that the handler manages its own limit (send.go uses config.MaxMessageSize),
|
||||
// so we skip the cap here for that route only.
|
||||
if _, ok := r.Context().Value(bodyLimitKey).(int64); !ok {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 5*1024*1024)
|
||||
}
|
||||
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
|
||||
// generate a new random nonce on every request
|
||||
|
||||
Reference in New Issue
Block a user