Skip to content

Commit cd6470a

Browse files
committed
fix(api): reject NaN/Inf in min-savings query params
parseMinSavingsParam used strconv.ParseFloat, which accepts "NaN", "Inf", "+Inf" and "Infinity" (case-insensitively), and only rejected v < 0, which is false for NaN. A NaN min_savings_usd bound into "monthly_savings >= $n" excludes every row with HTTP 200, and a NaN min_savings_pct is a silent no-op floor. Reject non-finite values at the input boundary with a 400 client error instead, per the fail-loud policy. Extends TestParseMinSavingsParam with NaN, +Inf, -Inf, bare/word infinity, and pct-path cases; the new cases fail on the pre-fix code. Closes #1183
1 parent 4108e51 commit cd6470a

2 files changed

Lines changed: 20 additions & 5 deletions

File tree

internal/api/validation.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package api
44
import (
55
"encoding/base64"
66
"fmt"
7+
"math"
78
"net/mail"
89
"regexp"
910
"strconv"
@@ -591,9 +592,10 @@ func decodeBase64Password(encoded string) (string, error) {
591592

592593
// parseMinSavingsParam parses a numeric savings-floor query parameter.
593594
// Returns (0, nil) when the parameter is absent or empty (no floor).
594-
// Returns 400 when the value is present but not a valid non-negative
595-
// integer or float. Fractional values are allowed (e.g. "12.5") since
596-
// savings floors can be sub-dollar amounts.
595+
// Returns 400 when the value is present but not a finite non-negative
596+
// integer or float (NaN and +/-Inf are rejected: a NaN floor silently
597+
// disables or inverts the filter downstream). Fractional values are
598+
// allowed (e.g. "12.5") since savings floors can be sub-dollar amounts.
597599
//
598600
// paramName is included in the error message so callers can distinguish
599601
// min_savings_usd vs min_savings_pct errors in client logs.
@@ -603,8 +605,8 @@ func parseMinSavingsParam(raw string, paramName string) (float64, error) {
603605
return 0, nil
604606
}
605607
v, err := strconv.ParseFloat(raw, 64)
606-
if err != nil {
607-
return 0, NewClientError(400, fmt.Sprintf("%s must be a non-negative number", paramName))
608+
if err != nil || math.IsNaN(v) || math.IsInf(v, 0) {
609+
return 0, NewClientError(400, fmt.Sprintf("%s must be a finite non-negative number", paramName))
608610
}
609611
if v < 0 {
610612
return 0, NewClientError(400, fmt.Sprintf("%s must be non-negative", paramName))

internal/api/validation_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,19 @@ func TestParseMinSavingsParam(t *testing.T) {
531531
{"non-numeric word", "thirty", "min_savings_usd", 0, true},
532532
{"negative value", "-5", "min_savings_usd", 0, true},
533533
{"mixed string", "30abc", "min_savings_usd", 0, true},
534+
535+
// Non-finite inputs (COR-09 regression, issue #1183):
536+
// strconv.ParseFloat accepts these case-insensitively, but a NaN
537+
// floor silently excludes all rows in SQL (monthly_savings >= NaN)
538+
// or applies no pct floor; +Inf excludes everything. All must 400.
539+
{"NaN", "NaN", "min_savings_usd", 0, true},
540+
{"lowercase nan", "nan", "min_savings_usd", 0, true},
541+
{"positive infinity", "+Inf", "min_savings_usd", 0, true},
542+
{"bare infinity", "Inf", "min_savings_usd", 0, true},
543+
{"infinity word", "Infinity", "min_savings_usd", 0, true},
544+
{"negative infinity", "-Inf", "min_savings_usd", 0, true},
545+
{"pct NaN", "NaN", "min_savings_pct", 0, true},
546+
{"pct infinity", "Inf", "min_savings_pct", 0, true},
534547
}
535548

536549
for _, tc := range cases {

0 commit comments

Comments
 (0)