Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent overflow of int values #89

Merged
merged 10 commits into from
Dec 2, 2024
5 changes: 5 additions & 0 deletions pkg/policy/ipld.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import (
"github.com/ipld/go-ipld-prime/must"
"github.com/ipld/go-ipld-prime/node/basicnode"

"github.com/ucan-wg/go-ucan/pkg/policy/limits"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
)

func FromIPLD(node datamodel.Node) (Policy, error) {
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
return nil, fmt.Errorf("policy contains integer values outside safe bounds: %w", err)
}

return statementsFromIPLD("/", node)
}

Expand Down
49 changes: 49 additions & 0 deletions pkg/policy/limits/int.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package limits

import (
"fmt"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/must"
)

const (
// MaxInt53 represents the maximum safe integer in JavaScript (2^53 - 1)
MaxInt53 = 9007199254740991
// MinInt53 represents the minimum safe integer in JavaScript (-2^53 + 1)
MinInt53 = -9007199254740991
)

func ValidateIntegerBoundsIPLD(node ipld.Node) error {
switch node.Kind() {
case ipld.Kind_Int:
val := must.Int(node)
if val > MaxInt53 || val < MinInt53 {
return fmt.Errorf("integer value %d exceeds safe bounds", val)
}
case ipld.Kind_List:
it := node.ListIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return err
}
if err := ValidateIntegerBoundsIPLD(v); err != nil {
return err
}
}
case ipld.Kind_Map:
it := node.MapIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return err
}
if err := ValidateIntegerBoundsIPLD(v); err != nil {
return err
}
}
}

return nil
}
82 changes: 82 additions & 0 deletions pkg/policy/limits/int_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package limits

import (
"testing"

"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/stretchr/testify/require"
)

func TestValidateIntegerBoundsIPLD(t *testing.T) {
buildMap := func() datamodel.Node {
nb := basicnode.Prototype.Any.NewBuilder()
qp.Map(1, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "foo", qp.Int(MaxInt53+1))
})(nb)
return nb.Build()
}

buildList := func() datamodel.Node {
nb := basicnode.Prototype.Any.NewBuilder()
qp.List(1, func(la datamodel.ListAssembler) {
qp.ListEntry(la, qp.Int(MinInt53-1))
})(nb)
return nb.Build()
}

tests := []struct {
name string
input datamodel.Node
wantErr bool
}{
{
name: "valid int",
input: basicnode.NewInt(42),
wantErr: false,
},
{
name: "max safe int",
input: basicnode.NewInt(MaxInt53),
wantErr: false,
},
{
name: "min safe int",
input: basicnode.NewInt(MinInt53),
wantErr: false,
},
{
name: "above MaxInt53",
input: basicnode.NewInt(MaxInt53 + 1),
wantErr: true,
},
{
name: "below MinInt53",
input: basicnode.NewInt(MinInt53 - 1),
wantErr: true,
},
{
name: "nested map with invalid int",
input: buildMap(),
wantErr: true,
},
{
name: "nested list with invalid int",
input: buildList(),
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateIntegerBoundsIPLD(tt.input)
if tt.wantErr {
require.Error(t, err)
require.Contains(t, err.Error(), "exceeds safe bounds")
} else {
require.NoError(t, err)
}
})
}
}
26 changes: 23 additions & 3 deletions pkg/policy/literal/literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/ipld/go-ipld-prime/fluent/qp"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode"

"github.com/ucan-wg/go-ucan/pkg/policy/limits"
)

var Bool = basicnode.NewBool
Expand Down Expand Up @@ -67,14 +69,21 @@ func Any(v any) (res ipld.Node, err error) {
case string:
return basicnode.NewString(val), nil
case int:
return basicnode.NewInt(int64(val)), nil
i := int64(val)
if i > limits.MaxInt53 || i < limits.MinInt53 {
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", i)
}
return basicnode.NewInt(i), nil
case int8:
return basicnode.NewInt(int64(val)), nil
case int16:
return basicnode.NewInt(int64(val)), nil
case int32:
return basicnode.NewInt(int64(val)), nil
case int64:
if val > limits.MaxInt53 || val < limits.MinInt53 {
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", val)
}
return basicnode.NewInt(val), nil
case uint:
return basicnode.NewInt(int64(val)), nil
Expand All @@ -85,6 +94,9 @@ func Any(v any) (res ipld.Node, err error) {
case uint32:
return basicnode.NewInt(int64(val)), nil
case uint64:
if val > uint64(limits.MaxInt53) {
return nil, fmt.Errorf("unsigned integer value %d exceeds safe integer bounds", val)
}
return basicnode.NewInt(int64(val)), nil
case float32:
return basicnode.NewFloat(float64(val)), nil
Expand Down Expand Up @@ -168,9 +180,17 @@ func anyAssemble(val any) qp.Assemble {
case reflect.Bool:
return qp.Bool(rv.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return qp.Int(rv.Int())
i := rv.Int()
if i > limits.MaxInt53 || i < limits.MinInt53 {
panic(fmt.Sprintf("integer %d exceeds safe bounds", i))
}
return qp.Int(i)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return qp.Int(int64(rv.Uint()))
u := rv.Uint()
if u > limits.MaxInt53 {
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
}
return qp.Int(int64(u))
case reflect.Float32, reflect.Float64:
return qp.Float(rv.Float())
case reflect.String:
Expand Down
54 changes: 53 additions & 1 deletion pkg/policy/literal/literal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/printer"
"github.com/stretchr/testify/require"

"github.com/ucan-wg/go-ucan/pkg/policy/limits"
)

func TestList(t *testing.T) {
Expand Down Expand Up @@ -214,7 +216,7 @@ func TestAny(t *testing.T) {
require.NoError(t, err)
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))

v, err = Any(data["func"])
_, err = Any(data["func"])
require.Error(t, err)
}

Expand Down Expand Up @@ -254,6 +256,56 @@ func BenchmarkAny(b *testing.B) {
})
}

func TestAnyAssembleIntegerOverflow(t *testing.T) {
tests := []struct {
name string
input interface{}
shouldErr bool
}{
{
name: "valid int",
input: 42,
shouldErr: false,
},
{
name: "max safe int",
input: limits.MaxInt53,
shouldErr: false,
},
{
name: "min safe int",
input: limits.MinInt53,
shouldErr: false,
},
{
name: "overflow int",
input: int64(limits.MaxInt53 + 1),
shouldErr: true,
},
{
name: "underflow int",
input: int64(limits.MinInt53 - 1),
shouldErr: true,
},
{
name: "overflow uint",
input: uint64(limits.MaxInt53 + 1),
shouldErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Any(tt.input)
if tt.shouldErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func must[T any](t T, err error) T {
if err != nil {
panic(err)
Expand Down
18 changes: 18 additions & 0 deletions pkg/policy/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package policy
import (
"cmp"
"fmt"
"math"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
Expand Down Expand Up @@ -249,10 +250,22 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
}

// isOrdered compares two IPLD nodes and returns true if they satisfy the given ordering function.
// It supports comparison of integers and floats, returning false for:
// - Nodes of different or unsupported kinds
// - Integer values outside JavaScript's safe integer bounds (±2^53-1)
// - Non-finite floating point values (NaN or ±Inf)
//
// The satisfies parameter is a function that interprets the comparison result:
// - For ">" it returns true when order is 1
// - For ">=" it returns true when order is 0 or 1
// - For "<" it returns true when order is -1
// - For "<=" it returns true when order is -1 or 0
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
a := must.Int(actual)
b := must.Int(expected)

return satisfies(cmp.Compare(a, b))
}

Expand All @@ -265,6 +278,11 @@ func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) b
if err != nil {
panic(fmt.Errorf("extracting selector float: %w", err))
}

if math.IsInf(a, 0) || math.IsNaN(a) || math.IsInf(b, 0) || math.IsNaN(b) {
return false
}

return satisfies(cmp.Compare(a, b))
}

Expand Down
11 changes: 11 additions & 0 deletions pkg/policy/selector/parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"regexp"
"strconv"
"strings"

"github.com/ucan-wg/go-ucan/pkg/policy/limits"
)

var (
Expand Down Expand Up @@ -56,6 +58,9 @@ func Parse(str string) (Selector, error) {
if err != nil {
return nil, newParseError("invalid index", str, col, tok)
}
if idx > limits.MaxInt53 || idx < limits.MinInt53 {
return nil, newParseError(fmt.Sprintf("index %d exceeds safe integer bounds", idx), str, col, tok)
}
sel = append(sel, segment{str: tok, optional: opt, index: idx})

// explicit field, ["abcd"]
Expand All @@ -77,6 +82,9 @@ func Parse(str string) (Selector, error) {
if err != nil {
return nil, newParseError("invalid slice index", str, col, tok)
}
if i > limits.MaxInt53 || i < limits.MinInt53 {
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
}
rng[0] = i
}
if splt[1] == "" {
Expand All @@ -86,6 +94,9 @@ func Parse(str string) (Selector, error) {
if err != nil {
return nil, newParseError("invalid slice index", str, col, tok)
}
if i > limits.MaxInt53 || i < limits.MinInt53 {
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
}
rng[1] = i
}
sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]})
Expand Down
Loading
Loading