Skip to content

Commit 72a358c

Browse files
SECVULN-29092 DoS handled for kvs_endpoint.go (#22916)
1 parent e285476 commit 72a358c

File tree

3 files changed

+129
-7
lines changed

3 files changed

+129
-7
lines changed

.changelog/22916.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:security
2+
security: Improved validation of the Content-Length header in the Consul KV endpoint to prevent potential denial of service attacks[CVE-2025-11374]()
3+
```

agent/kvs_endpoint.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package agent
55

66
import (
77
"bytes"
8+
"errors"
89
"fmt"
910
"io"
1011
"net/http"
@@ -238,19 +239,45 @@ func (s *HTTPHandlers) KVSPut(resp http.ResponseWriter, req *http.Request, args
238239
}
239240

240241
// Check the content-length
241-
if req.ContentLength > int64(s.agent.config.KVMaxValueSize) {
242+
maxSize := int64(s.agent.config.KVMaxValueSize)
243+
var buf *bytes.Buffer
244+
245+
switch {
246+
case req.ContentLength <= 0 && req.Body == nil:
247+
return "Request has no content-length & no body", nil
248+
249+
case req.ContentLength <= 0 && req.Body != nil:
250+
// LimitReader to limit copy of large requests with no Content-Length
251+
//+1 ensures we can detect if the body is too large
252+
byteReader := http.MaxBytesReader(nil, req.Body, maxSize+1)
253+
buf = new(bytes.Buffer)
254+
if _, err := io.Copy(buf, byteReader); err != nil {
255+
var bodyTooLargeErr *http.MaxBytesError
256+
if errors.As(err, &bodyTooLargeErr) {
257+
return nil, HTTPError{
258+
StatusCode: http.StatusRequestEntityTooLarge,
259+
Reason: fmt.Sprintf("Request body too large. Max allowed is %d bytes.", maxSize),
260+
}
261+
}
262+
return nil, err
263+
}
264+
265+
case req.ContentLength > maxSize:
266+
// Throw error if Content-Length is greater than max size
242267
return nil, HTTPError{
243268
StatusCode: http.StatusRequestEntityTooLarge,
244269
Reason: fmt.Sprintf("Request body(%d bytes) too large, max size: %d bytes. See %s.",
245-
req.ContentLength, s.agent.config.KVMaxValueSize, "https://developer.hashicorp.com/docs/agent/config/config-files#kv_max_value_size"),
270+
req.ContentLength, maxSize, "https://developer.hashicorp.com/docs/agent/config/config-files#kv_max_value_size"),
246271
}
247-
}
248272

249-
// Copy the value
250-
buf := bytes.NewBuffer(nil)
251-
if _, err := io.Copy(buf, req.Body); err != nil {
252-
return nil, err
273+
default:
274+
// Copy the value
275+
buf = bytes.NewBuffer(nil)
276+
if _, err := io.Copy(buf, req.Body); err != nil {
277+
return nil, err
278+
}
253279
}
280+
254281
applyReq.DirEnt.Value = buf.Bytes()
255282

256283
// Make the RPC

agent/kvs_endpoint_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package agent
66
import (
77
"bytes"
88
"fmt"
9+
"io"
910
"net/http"
1011
"net/http/httptest"
1112
"path"
@@ -37,6 +38,7 @@ func TestKVSEndpoint_PUT_GET_DELETE(t *testing.T) {
3738
for _, key := range keys {
3839
buf := bytes.NewBuffer([]byte("test"))
3940
req, _ := http.NewRequest("PUT", "/v1/kv/"+key, buf)
41+
req.ContentLength = int64(buf.Len())
4042
resp := httptest.NewRecorder()
4143
obj, err := a.srv.KVSEndpoint(resp, req)
4244
if err != nil {
@@ -554,6 +556,96 @@ func TestKVSEndpoint_GET(t *testing.T) {
554556
}
555557
}
556558

559+
func TestKVSPUT_SwitchCases(t *testing.T) {
560+
if testing.Short() {
561+
t.Skip("skipping test in short mode")
562+
}
563+
564+
t.Parallel()
565+
a := NewTestAgent(t, "")
566+
defer a.Shutdown()
567+
568+
maxSize := int(a.srv.agent.config.KVMaxValueSize)
569+
570+
tests := []struct {
571+
name string
572+
body string
573+
contentLength int64
574+
expectErr bool
575+
expectMsg string
576+
expectHTTPMsg string
577+
}{
578+
{
579+
name: "Case 2: No Content-Length but Body exists (allowed size)",
580+
body: "small-value",
581+
contentLength: 0,
582+
expectErr: false,
583+
},
584+
{
585+
name: "Case 2b: No Content-Length but Body exists (too large)",
586+
body: strings.Repeat("x", maxSize+50),
587+
contentLength: 0,
588+
expectErr: true,
589+
expectHTTPMsg: fmt.Sprintf("Request body too large. Max allowed is %d bytes.", maxSize),
590+
},
591+
{
592+
name: "Case 3: Content-Length greater than max allowed limit",
593+
body: strings.Repeat("x", maxSize+10),
594+
contentLength: int64(maxSize) + 10,
595+
expectErr: true,
596+
expectHTTPMsg: fmt.Sprintf("Request body(%d bytes) too large, max size: %d bytes.", int64(maxSize)+10, maxSize),
597+
},
598+
{
599+
name: "Case 4: Normal body within allowed limit",
600+
body: "tiny",
601+
contentLength: 4,
602+
expectErr: false,
603+
},
604+
}
605+
606+
for _, tt := range tests {
607+
t.Run(tt.name, func(t *testing.T) {
608+
var bodyReader io.Reader
609+
if tt.body != "" {
610+
bodyReader = bytes.NewBufferString(tt.body)
611+
} else {
612+
bodyReader = nil
613+
}
614+
615+
req := httptest.NewRequest(http.MethodPut, "/v1/kv/switch-test", bodyReader)
616+
req.ContentLength = tt.contentLength
617+
resp := httptest.NewRecorder()
618+
619+
obj, err := a.srv.KVSEndpoint(resp, req)
620+
621+
// Expected error cases
622+
if tt.expectErr {
623+
if err == nil {
624+
t.Fatalf("expected error, got nil")
625+
}
626+
httpErr, ok := err.(HTTPError)
627+
if !ok {
628+
t.Fatalf("expected HTTPError, got %T", err)
629+
}
630+
if !strings.Contains(httpErr.Reason, tt.expectHTTPMsg[:20]) { // partial match
631+
t.Fatalf("expected HTTPError reason to contain %q, got %q", tt.expectHTTPMsg, httpErr.Reason)
632+
}
633+
return
634+
}
635+
636+
// Unexpected error
637+
if err != nil {
638+
t.Fatalf("unexpected error: %v", err)
639+
}
640+
641+
// Normal successful PUT result
642+
if res, ok := obj.(bool); !ok || !res {
643+
t.Fatalf("expected successful PUT result, got %v", obj)
644+
}
645+
})
646+
}
647+
}
648+
557649
func TestKVSEndpoint_DELETE_ConflictingFlags(t *testing.T) {
558650
if testing.Short() {
559651
t.Skip("too slow for testing.Short")

0 commit comments

Comments
 (0)