Skip to content

Commit

Permalink
Merge pull request #89 from ucan-wg/fix/prevent-int-overflow
Browse files Browse the repository at this point in the history
fix: prevent overflow of int values
  • Loading branch information
MichaelMure authored Dec 2, 2024
2 parents dff52f8 + da806b1 commit 78d37d9
Show file tree
Hide file tree
Showing 16 changed files with 514 additions and 16 deletions.
16 changes: 16 additions & 0 deletions pkg/args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/printer"

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

Expand Down Expand Up @@ -62,6 +63,10 @@ func (a *Args) Add(key string, val any) error {
return err
}

if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
return fmt.Errorf("value for key %q: %w", key, err)
}

a.Values[key] = node
a.Keys = append(a.Keys, key)

Expand Down Expand Up @@ -164,3 +169,14 @@ func (a *Args) Clone() *Args {
}
return res
}

// Validate checks that all values in the Args are valid according to UCAN specs
func (a *Args) Validate() error {
for key, value := range a.Values {
if err := limits.ValidateIntegerBoundsIPLD(value); err != nil {
return fmt.Errorf("value for key %q: %w", key, err)
}
}

return nil
}
66 changes: 66 additions & 0 deletions pkg/args/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require"

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

Expand Down Expand Up @@ -185,6 +186,71 @@ func TestInclude(t *testing.T) {
}, maps.Collect(a1.Iter()))
}

func TestArgsIntegerBounds(t *testing.T) {
t.Parallel()

tests := []struct {
name string
key string
val int64
wantErr string
}{
{
name: "valid int",
key: "valid",
val: 42,
},
{
name: "max safe integer",
key: "max",
val: limits.MaxInt53,
},
{
name: "min safe integer",
key: "min",
val: limits.MinInt53,
},
{
name: "exceeds max safe integer",
key: "tooBig",
val: limits.MaxInt53 + 1,
wantErr: "exceeds safe integer bounds",
},
{
name: "below min safe integer",
key: "tooSmall",
val: limits.MinInt53 - 1,
wantErr: "exceeds safe integer bounds",
},
{
name: "duplicate key",
key: "duplicate",
val: 42,
wantErr: "duplicate key",
},
}

a := args.New()
require.NoError(t, a.Add("duplicate", 1)) // tests duplicate key

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := a.Add(tt.key, tt.val)
if tt.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err)
val, err := a.GetNode(tt.key)
require.NoError(t, err)
i, err := val.AsInt()
require.NoError(t, err)
require.Equal(t, tt.val, i)
}
})
}
}

const (
argsSchema = "type Args { String : Any }"
argsName = "Args"
Expand Down
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)
}
})
}
}
28 changes: 23 additions & 5 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 @@ -58,23 +60,28 @@ func List[T any](l []T) (ipld.Node, error) {
// Any creates an IPLD node from any value
// If possible, use another dedicated function for your type for performance.
func Any(v any) (res ipld.Node, err error) {
// TODO: handle uint overflow below

// some fast path
switch val := v.(type) {
case bool:
return basicnode.NewBool(val), nil
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 +92,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 +178,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
Loading

0 comments on commit 78d37d9

Please sign in to comment.