mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-06-01 14:21:05 +00:00
Harden Proxmox numeric parsing bounds
This commit is contained in:
parent
3c0707751b
commit
72559f737b
2 changed files with 147 additions and 17 deletions
|
|
@ -46,7 +46,11 @@ func (f *FlexInt) UnmarshalJSON(data []byte) error {
|
|||
// Try as float (handles cpulimit like 1.5)
|
||||
var fl float64
|
||||
if err := json.Unmarshal(data, &fl); err == nil {
|
||||
*f = FlexInt(int(fl))
|
||||
parsed, err := floatToIntTrunc(fl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = FlexInt(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -57,16 +61,72 @@ func (f *FlexInt) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
|
||||
// Parse string to float first (handles "1.5" format from cpulimit)
|
||||
floatVal, err := strconv.ParseFloat(s, 64)
|
||||
parsed, err := parseFlexibleIntString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert to int
|
||||
*f = FlexInt(int(floatVal))
|
||||
*f = FlexInt(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFlexibleIntString(raw string) (int, error) {
|
||||
if strings.ContainsAny(raw, ".eE") {
|
||||
parsed, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return floatToIntTrunc(parsed)
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64ToInt(parsed)
|
||||
}
|
||||
|
||||
func int64ToInt(v int64) (int, error) {
|
||||
maxInt := int64(^uint(0) >> 1)
|
||||
minInt := -maxInt - 1
|
||||
if v > maxInt || v < minInt {
|
||||
return 0, fmt.Errorf("integer %d exceeds int range", v)
|
||||
}
|
||||
return int(v), nil
|
||||
}
|
||||
|
||||
func floatToIntTrunc(v float64) (int, error) {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return 0, fmt.Errorf("non-finite float %v cannot be converted to int", v)
|
||||
}
|
||||
limit := math.Exp2(float64(strconv.IntSize - 1))
|
||||
if v >= limit || v < -limit {
|
||||
return 0, fmt.Errorf("float %v exceeds int range", v)
|
||||
}
|
||||
return int(v), nil
|
||||
}
|
||||
|
||||
func looksLikeNumericLiteral(raw string) bool {
|
||||
hasDigit := false
|
||||
var prev rune
|
||||
for i, r := range raw {
|
||||
switch {
|
||||
case unicode.IsDigit(r):
|
||||
hasDigit = true
|
||||
case r == '.' || r == 'e' || r == 'E':
|
||||
case r == '+' || r == '-':
|
||||
if i != 0 && prev != 'e' && prev != 'E' {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
prev = r
|
||||
}
|
||||
return hasDigit
|
||||
}
|
||||
|
||||
func coerceUint64(field string, value interface{}) (uint64, error) {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
|
|
@ -1563,10 +1623,7 @@ func parseUint64Flexible(value interface{}) (uint64, error) {
|
|||
}
|
||||
return uint64(v), nil
|
||||
case float64:
|
||||
if v < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return uint64(v), nil
|
||||
return floatToUint64Trunc(v)
|
||||
case json.Number:
|
||||
return parseUint64Flexible(v.String())
|
||||
case string:
|
||||
|
|
@ -1586,10 +1643,7 @@ func parseUint64Flexible(value interface{}) (uint64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if f < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return uint64(f), nil
|
||||
return floatToUint64Trunc(f)
|
||||
}
|
||||
u, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
|
|
@ -1601,6 +1655,19 @@ func parseUint64Flexible(value interface{}) (uint64, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func floatToUint64Trunc(v float64) (uint64, error) {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return 0, fmt.Errorf("non-finite float %v cannot be converted to uint64", v)
|
||||
}
|
||||
if v < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if v >= math.Exp2(64) {
|
||||
return 0, fmt.Errorf("float %v exceeds uint64 range", v)
|
||||
}
|
||||
return uint64(v), nil
|
||||
}
|
||||
|
||||
type VMIPAddress struct {
|
||||
Address string `json:"ip-address"`
|
||||
Prefix int `json:"prefix"`
|
||||
|
|
@ -2013,7 +2080,12 @@ func (d *Disk) UnmarshalJSON(data []byte) error {
|
|||
// Handle wearout field which can be int, string ("N/A"), or null
|
||||
switch v := aux.Wearout.(type) {
|
||||
case float64:
|
||||
d.Wearout = int(v)
|
||||
parsed, err := floatToIntTrunc(v)
|
||||
if err != nil {
|
||||
d.Wearout = wearoutUnknown
|
||||
break
|
||||
}
|
||||
d.Wearout = parsed
|
||||
case string:
|
||||
// Proxmox returns "N/A" or empty string for HDDs/RAID controllers.
|
||||
// Some controllers also return numeric wearout values as strings, so try to parse them.
|
||||
|
|
@ -2030,7 +2102,12 @@ func (d *Disk) UnmarshalJSON(data []byte) error {
|
|||
// Handle rpm field which can be number, string descriptor ("SSD"/"N/A"), or null
|
||||
switch v := aux.RPM.(type) {
|
||||
case float64:
|
||||
d.RPM = int(v)
|
||||
parsed, err := floatToIntTrunc(v)
|
||||
if err != nil {
|
||||
d.RPM = 0
|
||||
break
|
||||
}
|
||||
d.RPM = parsed
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" || strings.EqualFold(trimmed, "ssd") || strings.EqualFold(trimmed, "n/a") {
|
||||
|
|
@ -2080,11 +2157,13 @@ func parseWearoutValue(raw string) int {
|
|||
return parsed
|
||||
}
|
||||
|
||||
if parsed, err := strconv.ParseFloat(cleaned, 64); err == nil {
|
||||
if parsed <= 0 {
|
||||
return int(parsed)
|
||||
if looksLikeNumericLiteral(cleaned) {
|
||||
if parsed, err := strconv.ParseFloat(cleaned, 64); err == nil {
|
||||
if value, convErr := floatToIntTrunc(parsed); convErr == nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return int(parsed)
|
||||
return wearoutUnknown
|
||||
}
|
||||
|
||||
var digits strings.Builder
|
||||
|
|
|
|||
|
|
@ -7,11 +7,23 @@ import (
|
|||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func platformIntOverflowString() string {
|
||||
if strconv.IntSize == 32 {
|
||||
return "2147483648"
|
||||
}
|
||||
return "9223372036854775808"
|
||||
}
|
||||
|
||||
func platformIntOverflowFloat() float64 {
|
||||
return math.Exp2(float64(strconv.IntSize - 1))
|
||||
}
|
||||
|
||||
func TestDiskUnmarshalWearout(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -180,6 +192,20 @@ func TestDiskUnmarshalJSON_UnexpectedRPMType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDiskUnmarshalJSON_OutOfRangeNumericFieldsNormalize(t *testing.T) {
|
||||
payload := fmt.Sprintf(`{"devpath":"/dev/sda","model":"Example","serial":"123","type":"hdd","health":"OK","wearout":%.0f,"size":1000,"rpm":%.0f}`, platformIntOverflowFloat(), platformIntOverflowFloat())
|
||||
var disk Disk
|
||||
if err := json.Unmarshal([]byte(payload), &disk); err != nil {
|
||||
t.Fatalf("unexpected error unmarshalling disk: %v", err)
|
||||
}
|
||||
if disk.Wearout != wearoutUnknown {
|
||||
t.Fatalf("wearout: got %d, want %d (unknown)", disk.Wearout, wearoutUnknown)
|
||||
}
|
||||
if disk.RPM != 0 {
|
||||
t.Fatalf("rpm: got %d, want 0", disk.RPM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMFileSystemUnmarshalFlexibleNumbers(t *testing.T) {
|
||||
t.Run("accepts numeric values", func(t *testing.T) {
|
||||
payload := `{"name":"rootfs","type":"zfs","mountpoint":"/","total-bytes":8589934592,"used-bytes":3221225472,"disk":[{"dev":"/dev/vtbd0p2"}]}`
|
||||
|
|
@ -541,6 +567,26 @@ func TestFlexIntUnmarshalJSON(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFlexIntUnmarshalJSONRejectsNonFiniteAndOverflow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{name: "string NaN", input: `"NaN"`},
|
||||
{name: "string positive infinity", input: `"Infinity"`},
|
||||
{name: "string overflow", input: fmt.Sprintf(`"%s"`, platformIntOverflowString())},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var f FlexInt
|
||||
if err := f.UnmarshalJSON([]byte(tc.input)); err == nil {
|
||||
t.Fatalf("expected error for %s, got nil", tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUint64Flexible(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -565,6 +611,9 @@ func TestParseUint64Flexible(t *testing.T) {
|
|||
{name: "float64 truncates down", value: float64(9.9), want: 9},
|
||||
{name: "float64 negative", value: float64(-1.5), want: 0},
|
||||
{name: "float64 zero", value: float64(0.0), want: 0},
|
||||
{name: "float64 NaN", value: math.NaN(), wantErr: true},
|
||||
{name: "float64 positive infinity", value: math.Inf(1), wantErr: true},
|
||||
{name: "float64 out of range", value: math.Exp2(64), wantErr: true},
|
||||
// json.Number
|
||||
{name: "json.Number integer", value: json.Number("99"), want: 99},
|
||||
{name: "json.Number float", value: json.Number("3.14"), want: 3},
|
||||
|
|
@ -934,6 +983,8 @@ func TestParseWearoutValue(t *testing.T) {
|
|||
{name: "float value truncated", raw: "81.5", want: 81},
|
||||
{name: "float zero decimal", raw: "90.0", want: 90},
|
||||
{name: "float high precision", raw: "75.999", want: 75},
|
||||
{name: "float non-finite string", raw: "NaN", want: wearoutUnknown},
|
||||
{name: "float overflow string", raw: "9.223372036854776e18", want: wearoutUnknown},
|
||||
{name: "negative float", raw: "-5.5", want: -5},
|
||||
{name: "float zero exactly", raw: "0.0", want: 0},
|
||||
{name: "float negative zero", raw: "-0.0", want: 0},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue