CVE-2024-35185: Insufficient Information (NVD-CWE-noinfo)
value: Minder is a software supply chain security platform. Prior to version 0.0.49, the Minder REST ingester is vulnerable to a denial of service attack via an attacker-controlled REST endpoint that can crash the Minder server. The REST ingester allows users to interact with REST endpoints to fetch data for rule evaluation. When fetching data with the REST ingester, Minder sends a request to an endpoint and will use the data from the body of the response as the data to evaluate against a certain rule. If the response is sufficiently large, it can drain memory on the machine and crash the Minder server. The attacker can control the remote REST endpoints that Minder sends requests to, and they can configure the remote REST endpoints to return responses with large bodies. They would then instruct Minder to send a request to their configured endpoint that would return the large response which would crash the Minder server. Version 0.0.49 fixes this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-16T16:15Z
Repository: https://github.com/stacklok/minder
Language: Go
Files changed: internal/engine/ingester/rest/rest.go, internal/engine/ingester/rest/rest_test.go
Commit date: 2024-05-16 09:49:03 +0300
Commit message: Merge pull request from GHSA-fjw8-3gp8-4cvx

This ensures that a malicious (or faulty) REST server does not crash
minder with too big requests. Minder will thus limit requests body reads
to 1Mb.

Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
Diff: diff --git a/internal/engine/ingester/rest/rest.go b/internal/engine/ingester/rest/rest.go
index 8965e34e3..61863d700 100644
--- a/internal/engine/ingester/rest/rest.go
+++ b/internal/engine/ingester/rest/rest.go
@@ -38,10 +38,14 @@ import (
 )
 
 const (
 	// RestRuleDataIngestType is the type of the REST rule data ingest engine
 	RestRuleDataIngestType = "rest"
+
+	// MaxBytesLimit is the maximum number of bytes to read from the response body
+	// We limit to 1MB to prevent abuse
+	MaxBytesLimit int64 = 1 << 20
 )
 
 type ingestorFallback struct {
 	// httpCode is the HTTP status code to return
 	httpCode int
@@ -199,20 +203,22 @@ func (rdi *Ingestor) parseBody(body io.Reader) (any, error) {
 
 	if body == nil {
 		return nil, nil
 	}
 
+	lr := io.LimitReader(body, MaxBytesLimit)
+
 	if rdi.restCfg.Parse == "json" {
 		var jsonData any
-		dec := json.NewDecoder(body)
+		dec := json.NewDecoder(lr)
 		if err := dec.Decode(&jsonData); err != nil {
 			return nil, fmt.Errorf("cannot decode json: %w", err)
 		}
 
 		data = jsonData
 	} else {
-		data, err = io.ReadAll(body)
+		data, err = io.ReadAll(lr)
 		if err != nil {
 			return nil, fmt.Errorf("cannot read response body: %w", err)
 		}
 	}
 
diff --git a/internal/engine/ingester/rest/rest_test.go b/internal/engine/ingester/rest/rest_test.go
index 9f444f8f4..ab97b9840 100644
--- a/internal/engine/ingester/rest/rest_test.go
+++ b/internal/engine/ingester/rest/rest_test.go
@@ -15,10 +15,11 @@
 
 package rest
 
 import (
 	"context"
+	"encoding/binary"
 	"encoding/json"
 	"net/http"
 	"net/http/httptest"
 	"strings"
 	"testing"
@@ -317,5 +318,56 @@ func TestRestIngest(t *testing.T) {
 			require.NoError(t, err, "unexpected error creating remediate engine")
 			require.Equal(t, tt.ingResultFn(), result, "unexpected result")
 		})
 	}
 }
+
+func TestIngestor_parseBodyDoesNotReadTooLargeRequests(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		cfg     *pb.RestType
+		body    string
+		wantErr bool
+	}{
+		{
+			name: "raw",
+			cfg:  &pb.RestType{},
+			// really large body
+			// casting to `int` should work.
+			body: strings.Repeat("a", int(MaxBytesLimit)+1),
+			// This case does not error, it simply truncates.
+			wantErr: false,
+		},
+		{
+			name: "json",
+			cfg: &pb.RestType{
+				Parse: "json",
+			},
+			// really large body
+			// casting to `int` should work.
+			body: "{\"a\":\"" + strings.Repeat("a", int(MaxBytesLimit)+1) + "\"}",
+			// This case will error out, as truncating
+			// makes the JSON invalid.
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			rdi := &Ingestor{
+				restCfg: tt.cfg,
+			}
+
+			got, err := rdi.parseBody(strings.NewReader(tt.body))
+			if tt.wantErr {
+				assert.Error(t, err, "expected error")
+			} else {
+				assert.NoError(t, err, "expected no error")
+				assert.Equal(t, int(MaxBytesLimit), binary.Size(got), "expected body to be truncated")
+			}
+		})
+	}
+}

Vulnerable code: 		dec := json.NewDecoder(body)
		data, err = io.ReadAll(body)
Fixed code: 
	// MaxBytesLimit is the maximum number of bytes to read from the response body
	// We limit to 1MB to prevent abuse
	MaxBytesLimit int64 = 1 << 20
	lr := io.LimitReader(body, MaxBytesLimit)

		dec := json.NewDecoder(lr)
		data, err = io.ReadAll(lr)
	"encoding/binary"

func TestIngestor_parseBodyDoesNotReadTooLargeRequests(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		cfg     *pb.RestType
		body    string
		wantErr bool
	}{
		{
			name: "raw",
			cfg:  &pb.RestType{},
			// really large body
			// casting to `int` should work.
			body: strings.Repeat("a", int(MaxBytesLimit)+1),
			// This case does not error, it simply truncates.
			wantErr: false,
		},
		{
			name: "json",
			cfg: &pb.RestType{
				Parse: "json",
			},
			// really large body
			// casting to `int` should work.
			body: "{\"a\":\"" + strings.Repeat("a", int(MaxBytesLimit)+1) + "\"}",
			// This case will error out, as truncating
			// makes the JSON invalid.
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			rdi := &Ingestor{
				restCfg: tt.cfg,
			}

			got, err := rdi.parseBody(strings.NewReader(tt.body))
			if tt.wantErr {
				assert.Error(t, err, "expected error")
			} else {
				assert.NoError(t, err, "expected no error")
				assert.Equal(t, int(MaxBytesLimit), binary.Size(got), "expected body to be truncated")
			}
		})
	}
}