diff --git a/cli/cmd/model_pull.go b/cli/cmd/model_pull.go index 9b8486668..aa9b7c091 100644 --- a/cli/cmd/model_pull.go +++ b/cli/cmd/model_pull.go @@ -109,11 +109,15 @@ func (r *runners) pullModel(cmd *cobra.Command, args []string) error { } defer manifestContent.Close() - // Read the manifest content into a byte slice - manifestBytes, err := io.ReadAll(manifestContent) + // Read the manifest content into a byte slice (OCI manifests are small JSON). + const maxManifestBody = 16 << 20 // 16 MiB + manifestBytes, err := io.ReadAll(io.LimitReader(manifestContent, maxManifestBody+1)) if err != nil { return err } + if len(manifestBytes) > maxManifestBody { + return fmt.Errorf("OCI manifest exceeds %d bytes", maxManifestBody) + } var manifest v1.Manifest if err := json.Unmarshal(manifestBytes, &manifest); err != nil { diff --git a/pkg/cmxmetadata/cmxmetadata.go b/pkg/cmxmetadata/cmxmetadata.go index 6e261bbec..e5bcb8cef 100644 --- a/pkg/cmxmetadata/cmxmetadata.go +++ b/pkg/cmxmetadata/cmxmetadata.go @@ -17,6 +17,8 @@ const ( mmdsPath = "/latest/vendor-api" mmdsTimeout = 500 * time.Millisecond // fail fast if not in CMX tokenLeeway = 60 * time.Second // refresh token this early before expiry + // maxMetadataBody bounds MMDS / token JSON responses (credentials, not archives). + maxMetadataBody = 1 << 20 // 1 MiB ) // ErrNotAvailable is returned when the CMX metadata service is not reachable. @@ -61,7 +63,7 @@ func GetVMMetadata() (*VMMetadata, error) { return nil, ErrNotAvailable } - body, err := io.ReadAll(resp.Body) + body, err := readAllLimited(resp.Body, maxMetadataBody) if err != nil { return nil, ErrNotAvailable } @@ -140,7 +142,7 @@ func exchangeCredentials(meta *VMMetadata) (string, *time.Time, error) { } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) + body, err := readAllLimited(resp.Body, maxMetadataBody) if err != nil { return "", nil, fmt.Errorf("reading token response body: %w", err) } @@ -166,3 +168,14 @@ func exchangeCredentials(meta *VMMetadata) (string, *time.Time, error) { return tokenResp.AccessToken, expiresAt, nil } + +func readAllLimited(r io.Reader, limit int64) ([]byte, error) { + body, err := io.ReadAll(io.LimitReader(r, limit+1)) + if err != nil { + return nil, err + } + if int64(len(body)) > limit { + return nil, fmt.Errorf("response body exceeds %d bytes", limit) + } + return body, nil +} diff --git a/pkg/credentials/fetch.go b/pkg/credentials/fetch.go index 8557e471f..a511d7982 100644 --- a/pkg/credentials/fetch.go +++ b/pkg/credentials/fetch.go @@ -122,10 +122,15 @@ func exchangeNonceForToken(uri string, nonce string) (string, error) { return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - b, err := io.ReadAll(resp.Body) + // Token JSON is small; bound allocation against a hostile exchange endpoint. + const maxTokenResponse = 1 << 20 // 1 MiB + b, err := io.ReadAll(io.LimitReader(resp.Body, maxTokenResponse+1)) if err != nil { return "", err } + if len(b) > maxTokenResponse { + return "", fmt.Errorf("token response exceeds %d bytes", maxTokenResponse) + } type tokenResponse struct { Token string `json:"token"` diff --git a/pkg/tools/checksum.go b/pkg/tools/checksum.go index 90f2ad2fe..844213f2d 100644 --- a/pkg/tools/checksum.go +++ b/pkg/tools/checksum.go @@ -15,6 +15,9 @@ var checksumHTTPClient = &http.Client{ Timeout: 30 * time.Second, } +// maxChecksumBody bounds checksum text files (not the archives themselves). +const maxChecksumBody = 1 << 20 // 1 MiB + // VerifyHelmChecksum verifies a Helm binary against its .sha256sum file func VerifyHelmChecksum(data []byte, archiveURL string) error { // Helm provides per-file checksums: .sha256sum @@ -31,7 +34,7 @@ func VerifyHelmChecksum(data []byte, archiveURL string) error { return fmt.Errorf("checksum file not found (HTTP %d): %s", resp.StatusCode, checksumURL) } - checksumData, err := io.ReadAll(resp.Body) + checksumData, err := readAllLimited(resp.Body, maxChecksumBody) if err != nil { return fmt.Errorf("reading checksum file: %w", err) } @@ -71,7 +74,7 @@ func VerifyTroubleshootChecksum(data []byte, version, filename string) error { return fmt.Errorf("checksums file not found (HTTP %d): %s", resp.StatusCode, checksumURL) } - checksumData, err := io.ReadAll(resp.Body) + checksumData, err := readAllLimited(resp.Body, maxChecksumBody) if err != nil { return fmt.Errorf("reading checksums file: %w", err) } @@ -102,3 +105,14 @@ func VerifyTroubleshootChecksum(data []byte, version, filename string) error { return nil } + +func readAllLimited(r io.Reader, limit int64) ([]byte, error) { + body, err := io.ReadAll(io.LimitReader(r, limit+1)) + if err != nil { + return nil, err + } + if int64(len(body)) > limit { + return nil, fmt.Errorf("response body exceeds %d bytes", limit) + } + return body, nil +} diff --git a/pkg/tools/checksum_limit_test.go b/pkg/tools/checksum_limit_test.go new file mode 100644 index 000000000..0a6a06b5f --- /dev/null +++ b/pkg/tools/checksum_limit_test.go @@ -0,0 +1,23 @@ +package tools + +import ( + "strings" + "testing" +) + +func TestReadAllLimited(t *testing.T) { + t.Parallel() + + got, err := readAllLimited(strings.NewReader("abc"), 10) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(got) != "abc" { + t.Fatalf("got %q, want abc", got) + } + + _, err = readAllLimited(strings.NewReader(strings.Repeat("x", 5)), 4) + if err == nil { + t.Fatal("expected error when body exceeds limit") + } +}