From 217b223bb770a6ccf1b934e3871dc2528687c53b Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Fri, 23 Oct 2026 22:22:34 +0530 Subject: [PATCH] fix(normalizers): reject integer parameters at or above 2^63 --- pkg/github/security_outputs.go | 3 +- pkg/github/typed_read_normalizers.go | 3 +- pkg/github/typed_search_outputs_test.go | 57 +++++++++++++++ pkg/github/typed_security_outputs_test.go | 85 +++++++++++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/pkg/github/security_outputs.go b/pkg/github/security_outputs.go index 88fb34f12b..ca3f15b37a 100644 --- a/pkg/github/security_outputs.go +++ b/pkg/github/security_outputs.go @@ -300,8 +300,9 @@ func normalizeSecurityIntegerArguments(fields ...string) inventory.InputNormaliz } } parsed, err := strconv.ParseFloat(number, 64) + limit := math.Ldexp(1, strconv.IntSize-1) if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || - parsed != math.Trunc(parsed) || parsed > float64(math.MaxInt) || parsed < float64(math.MinInt) { + parsed != math.Trunc(parsed) || parsed >= limit || parsed < -limit { return nil, fmt.Errorf("parameter %s is not a valid number", field) } arguments[field], err = json.Marshal(int(parsed)) diff --git a/pkg/github/typed_read_normalizers.go b/pkg/github/typed_read_normalizers.go index 1bec77b7ce..6fab3232ac 100644 --- a/pkg/github/typed_read_normalizers.go +++ b/pkg/github/typed_read_normalizers.go @@ -42,7 +42,8 @@ func normalizeTypedReadArguments(uppercaseFields []string, preserveZeroPage bool } if text, ok := number.(string); ok { parsed, err := strconv.ParseFloat(text, 64) - if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed != math.Trunc(parsed) || parsed > float64(math.MaxInt) || parsed < float64(math.MinInt) { + limit := math.Ldexp(1, strconv.IntSize-1) + if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed != math.Trunc(parsed) || parsed >= limit || parsed < -limit { return nil, fmt.Errorf("parameter %s is not a valid number", field) } number = int(parsed) diff --git a/pkg/github/typed_search_outputs_test.go b/pkg/github/typed_search_outputs_test.go index 93aab4ff99..2f8d2eaed7 100644 --- a/pkg/github/typed_search_outputs_test.go +++ b/pkg/github/typed_search_outputs_test.go @@ -383,3 +383,60 @@ func mustMarshalJSON(t *testing.T, value any) string { require.NoError(t, err) return string(encoded) } + +func TestNormalizeTypedReadArgumentsRejectsOutOfRangeIntegers(t *testing.T) { + normalize := normalizeTypedReadArguments(nil, false) + + tests := []struct { + name string + input string + expected string + expectError bool + }{ + { + name: "valid perPage string", + input: `{"perPage": "50"}`, + expected: `{"perPage": 50}`, + expectError: false, + }, + { + name: "valid page string", + input: `{"page": "3"}`, + expected: `{"page": 3}`, + expectError: false, + }, + { + name: "2^63 string page parameter rejected", + input: `{"page": "9223372036854775808"}`, + expectError: true, + }, + { + name: "MaxInt64 string perPage parameter rounds up to 2^63 rejected", + input: `{"perPage": "9223372036854775807"}`, + expectError: true, + }, + { + name: "non-integer string rejected", + input: `{"perPage": "12.5"}`, + expectError: true, + }, + { + name: "invalid syntax rejected", + input: `{"page": "invalid"}`, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + normalized, err := normalize(json.RawMessage(tc.input)) + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid number") + } else { + require.NoError(t, err) + assert.JSONEq(t, tc.expected, string(normalized)) + } + }) + } +} diff --git a/pkg/github/typed_security_outputs_test.go b/pkg/github/typed_security_outputs_test.go index e793fcbbd2..c125e3b0f5 100644 --- a/pkg/github/typed_security_outputs_test.go +++ b/pkg/github/typed_security_outputs_test.go @@ -390,3 +390,88 @@ func TestSecurityOutputDTOs(t *testing.T) { check(typ) } } + +func TestNormalizeSecurityIntegerArguments(t *testing.T) { + normalize := normalizeSecurityIntegerArguments("alertNumber", "page") + + tests := []struct { + name string + input string + expected string + expectError bool + }{ + { + name: "valid numeric integer", + input: `{"alertNumber": 42}`, + expected: `{"alertNumber": 42}`, + expectError: false, + }, + { + name: "valid string integer", + input: `{"alertNumber": "42"}`, + expected: `{"alertNumber": 42}`, + expectError: false, + }, + { + name: "valid MaxInt32 string", + input: `{"alertNumber": "2147483647"}`, + expected: `{"alertNumber": 2147483647}`, + expectError: false, + }, + { + name: "2^63 string parameter rejected", + input: `{"alertNumber": "9223372036854775808"}`, + expectError: true, + }, + { + name: "MaxInt64 string parameter rounds up to 2^63 rejected", + input: `{"alertNumber": "9223372036854775807"}`, + expectError: true, + }, + { + name: "2^63 numeric parameter rejected", + input: `{"alertNumber": 9223372036854775808}`, + expectError: true, + }, + { + name: "non-integer string rejected", + input: `{"alertNumber": "1.5"}`, + expectError: true, + }, + { + name: "invalid syntax string rejected", + input: `{"alertNumber": "abc"}`, + expectError: true, + }, + { + name: "null value rejected", + input: `{"alertNumber": null}`, + expectError: true, + }, + { + name: "omitted field preserved", + input: `{"other": "value"}`, + expected: `{"other": "value"}`, + expectError: false, + }, + { + name: "multiple fields normalized", + input: `{"alertNumber": "10", "page": "2"}`, + expected: `{"alertNumber": 10, "page": 2}`, + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + normalized, err := normalize(json.RawMessage(tc.input)) + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid number") + } else { + require.NoError(t, err) + assert.JSONEq(t, tc.expected, string(normalized)) + } + }) + } +}