Skip to content

Commit 5754c82

Browse files
committed
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.
1 parent fdf3cde commit 5754c82

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

server/apiv1/send.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
4747
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxMessageSize)*1024*1024)
4848
}
4949

50-
decoder := json.NewDecoder(r.Body)
51-
52-
data := sendMessageParams{}
53-
54-
if err := decoder.Decode(&data.Body); err != nil {
50+
// Read body before decoding so that MaxBytesReader errors are returned directly.
51+
// In Go 1.26+, json.Decoder wraps reader errors in *json.SyntaxError, which
52+
// prevents errors.As from finding *http.MaxBytesError to return a 413.
53+
body, err := io.ReadAll(r.Body)
54+
if err != nil {
5555
var maxErr *http.MaxBytesError
5656
if errors.As(err, &maxErr) {
5757
w.WriteHeader(http.StatusRequestEntityTooLarge)
@@ -60,6 +60,13 @@ func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
6060
return
6161
}
6262

63+
data := sendMessageParams{}
64+
65+
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&data.Body); err != nil {
66+
httpJSONError(w, err.Error())
67+
return
68+
}
69+
6370
var httpAuthUser *string
6471
if user, _, ok := r.BasicAuth(); ok {
6572
httpAuthUser = &user

server/server.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@ var (
4141
// auth.UICredentials pointer (which is a data race under concurrent load).
4242
type contextKey int
4343

44-
const skipUIAuthKey contextKey = iota
44+
const (
45+
skipUIAuthKey contextKey = iota
46+
// bodyLimitKey carries an optional request body size cap (in bytes) through the
47+
// context. middleWareFunc reads it and applies it instead of the default 5 MB cap.
48+
// A value of 0 means unlimited. Used by sendAPIAuthMiddleware to honour
49+
// config.MaxMessageSize for the send endpoint.
50+
bodyLimitKey
51+
)
4552

4653
// Listen will start the httpd
4754
func Listen() {
@@ -232,6 +239,10 @@ func basicAuthResponse(w http.ResponseWriter) {
232239
// auth.UICredentials pointer, which would be a data race under concurrent load.
233240
func sendAPIAuthMiddleware(fn http.HandlerFunc) http.HandlerFunc {
234241
return func(w http.ResponseWriter, r *http.Request) {
242+
// Override the default 5 MB body cap with the send-specific limit so that
243+
// middleWareFunc applies config.MaxMessageSize (0 = unlimited) instead.
244+
r = r.WithContext(context.WithValue(r.Context(), bodyLimitKey, int64(config.MaxMessageSize)*1024*1024))
245+
235246
// If send API auth accept any is enabled, bypass all authentication.
236247
if config.SendAPIAuthAcceptAny {
237248
ctx := context.WithValue(r.Context(), skipUIAuthKey, true)
@@ -277,6 +288,14 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
277288
// and gzip compression.
278289
func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
279290
return func(w http.ResponseWriter, r *http.Request) {
291+
// Limit request body size to 5 MB to prevent memory-exhaustion DoS via large
292+
// JSON bodies. sendAPIAuthMiddleware sets bodyLimitKey in the context to signal
293+
// that the handler manages its own limit (send.go uses config.MaxMessageSize),
294+
// so we skip the cap here for that route only.
295+
if _, ok := r.Context().Value(bodyLimitKey).(int64); !ok {
296+
r.Body = http.MaxBytesReader(w, r.Body, 5*1024*1024)
297+
}
298+
280299
w.Header().Set("Referrer-Policy", "no-referrer")
281300

282301
// generate a new random nonce on every request

0 commit comments

Comments
 (0)