Skip to content

Commit

Permalink
[safecast] Introduced utilities to perform casting safely (#511)
Browse files Browse the repository at this point in the history
<!--
Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors.
All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->
### Description

protect against
[CWE-190](https://cwe.mitre.org/data/definitions/190.html)


### Test Coverage

<!--
Please put an `x` in the correct box e.g. `[x]` to indicate the testing
coverage of this change.
-->

- [x]  This change is covered by existing or additional automated tests.
- [ ] Manual testing has been performed (and evidence provided) as
automated testing was not feasible.
- [ ] Additional tests are not required for this change (e.g.
documentation update).
  • Loading branch information
acabarbaye authored Nov 7, 2024
1 parent 87ce671 commit 878f149
Show file tree
Hide file tree
Showing 15 changed files with 897 additions and 24 deletions.
1 change: 1 addition & 0 deletions changes/20241107160700.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[safecast]` Introduced utilities to perform casting safely and protect against [CWE-190](https://cwe.mitre.org/data/definitions/190.html)
22 changes: 12 additions & 10 deletions utils/field/fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/go-faker/faker/v4"
"github.com/stretchr/testify/assert"

"github.com/ARM-software/golang-utils/utils/safecast"
)

func TestOptionalField(t *testing.T) {
Expand All @@ -22,7 +24,7 @@ func TestOptionalField(t *testing.T) {
}{
{
fieldType: "Int",
value: time.Now().Second(),
value: safecast.ToInt(time.Now().Second()),
defaultValue: 76,
setFunction: func(a any) any {
return ToOptionalInt(a.(int))
Expand All @@ -37,8 +39,8 @@ func TestOptionalField(t *testing.T) {
},
{
fieldType: "UInt",
value: uint(time.Now().Second()), //nolint:gosec // time is positive and uint has more bits than int so no overflow
defaultValue: uint(76),
value: safecast.ToUint(time.Now().Second()),
defaultValue: safecast.ToUint(76),
setFunction: func(a any) any {
return ToOptionalUint(a.(uint))
},
Expand All @@ -52,8 +54,8 @@ func TestOptionalField(t *testing.T) {
},
{
fieldType: "Int32",
value: int32(time.Now().Second()), //nolint:gosec // this should be okay until 2038
defaultValue: int32(97894),
value: safecast.ToInt32(time.Now().Second()),
defaultValue: safecast.ToInt32(97894),
setFunction: func(a any) any {
return ToOptionalInt32(a.(int32))
},
Expand All @@ -67,8 +69,8 @@ func TestOptionalField(t *testing.T) {
},
{
fieldType: "UInt32",
value: uint32(time.Now().Second()), //nolint:gosec // this should be okay until 2038
defaultValue: uint32(97894),
value: safecast.ToUint32(time.Now().Second()),
defaultValue: safecast.ToUint32(97894),
setFunction: func(a any) any {
return ToOptionalUint32(a.(uint32))
},
Expand All @@ -83,7 +85,7 @@ func TestOptionalField(t *testing.T) {
{
fieldType: "Int64",
value: time.Now().Unix(),
defaultValue: int64(97894),
defaultValue: safecast.ToInt64(97894),
setFunction: func(a any) any {
return ToOptionalInt64(a.(int64))
},
Expand All @@ -97,8 +99,8 @@ func TestOptionalField(t *testing.T) {
},
{
fieldType: "UInt64",
value: uint64(time.Now().Unix()), //nolint:gosec // time is positive and uint64 has more bits than int64 so no overflow
defaultValue: uint64(97894),
value: safecast.ToUint64(time.Now().Unix()),
defaultValue: safecast.ToUint64(97894),
setFunction: func(a any) any {
return ToOptionalUint64(a.(uint64))
},
Expand Down
7 changes: 4 additions & 3 deletions utils/filesystem/zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/ARM-software/golang-utils/utils/collection"
"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/parallelisation"
"github.com/ARM-software/golang-utils/utils/safecast"
"github.com/ARM-software/golang-utils/utils/safeio"
)

Expand Down Expand Up @@ -356,16 +357,16 @@ func (fs *VFS) unzip(ctx context.Context, source string, destination string, lim
fileCounter.Inc()
fileList = append(fileList, filePath)
}
totalSizeOnDisk.Add(uint64(fileSizeOnDisk)) //nolint:gosec // file size is positive and uint64 has more bits than int64 so no overflow
totalSizeOnDisk.Add(safecast.ToUint64(fileSizeOnDisk))
}
} else {
totalSizeOnDisk.Add(uint64(fileSizeOnDisk)) //nolint:gosec // file size is positive and uint64 has more bits than int64 so no overflow
totalSizeOnDisk.Add(safecast.ToUint64(fileSizeOnDisk))
}

if limits.Apply() && totalSizeOnDisk.Load() > limits.GetMaxTotalSize() {
return fileList, fileCounter.Load(), totalSizeOnDisk.Load(), fmt.Errorf("%w: more than %v B of disk space was used while unzipping %v (%v B used already)", commonerrors.ErrTooLarge, limits.GetMaxTotalSize(), source, totalSizeOnDisk.Load())
}
if filecount := fileCounter.Load(); limits.Apply() && filecount <= math.MaxInt64 && int64(filecount) > limits.GetMaxFileCount() { //nolint:gosec // if filecount of uint64 is greater than the max value of int64 then it must be greater than GetMaxFileCount as that is an int64
if filecount := fileCounter.Load(); limits.Apply() && filecount <= math.MaxInt64 && safecast.ToInt64(filecount) > limits.GetMaxFileCount() {
return fileList, filecount, totalSizeOnDisk.Load(), fmt.Errorf("%w: more than %v files were created while unzipping %v (%v files created already)", commonerrors.ErrTooLarge, limits.GetMaxFileCount(), source, filecount)
}
}
Expand Down
10 changes: 8 additions & 2 deletions utils/idgen/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
*/
package idgen

import "github.com/gofrs/uuid"
import (
"fmt"

"github.com/gofrs/uuid"

"github.com/ARM-software/golang-utils/utils/commonerrors"
)

// Generates a UUID.
func GenerateUUID4() (string, error) {
uuid, err := uuid.NewV4()
if err != nil {
return "", err
return "", fmt.Errorf("%w: failed generating uuid: %v", commonerrors.ErrUnexpected, err.Error())
}
return uuid.String(), nil
}
Expand Down
6 changes: 3 additions & 3 deletions utils/idgen/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ import (

func TestUuidUniqueness(t *testing.T) {
uuid1, err := GenerateUUID4()
require.Nil(t, err)
require.NoError(t, err)

uuid2, err := GenerateUUID4()
require.Nil(t, err)
require.NoError(t, err)

assert.NotEqual(t, uuid1, uuid2)
}

func TestUuidLength(t *testing.T) {
uuid, err := GenerateUUID4()
require.Nil(t, err)
require.NoError(t, err)

assert.Equal(t, 36, len(uuid))
}
Expand Down
5 changes: 3 additions & 2 deletions utils/platform/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/shirou/gopsutil/v3/mem"

"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/safecast"
)

var (
Expand Down Expand Up @@ -114,7 +115,7 @@ func UpTime() (uptime time.Duration, err error) {
err = fmt.Errorf("%w: could not convert uptime '%v' to duration as it exceeds the upper limit for time.Duration", commonerrors.ErrOutOfRange, _uptime)
return
}
uptime = time.Duration(_uptime) * time.Second //nolint:gosec // we have verified the value of _uptime is whithin the upper limit for time.Duration in the above check
uptime = time.Duration(safecast.ToInt64(_uptime)) * time.Second
return
}

Expand All @@ -128,7 +129,7 @@ func BootTime() (bootime time.Time, err error) {
err = fmt.Errorf("%w: could not convert uptime '%v' to duration as it exceeds the upper limit for time.Duration", commonerrors.ErrOutOfRange, _bootime)
return
}
bootime = time.Unix(int64(_bootime), 0) //nolint:gosec // we have verified the value of _bootime is whithin the upper limit for time.Duration in the above check
bootime = time.Unix(safecast.ToInt64(_bootime), 0)
return

}
Expand Down
3 changes: 2 additions & 1 deletion utils/proc/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/ARM-software/golang-utils/utils/collection"
"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/parallelisation"
"github.com/ARM-software/golang-utils/utils/safecast"
)

const (
Expand Down Expand Up @@ -245,7 +246,7 @@ func isProcessRunning(p *process.Process) (running bool) {
// to get more information about the process. An error will be returned
// if the process does not exist.
func NewProcess(ctx context.Context, pid int) (pr IProcess, err error) {
p, err := process.NewProcessWithContext(ctx, int32(pid)) //nolint:gosec // Max PID is 2^22 which is within int32 range https://stackoverflow.com/a/6294196
p, err := process.NewProcessWithContext(ctx, safecast.ToInt32(pid))
err = ConvertProcessError(err)
if err != nil {
return
Expand Down
4 changes: 2 additions & 2 deletions utils/reflection/reflection.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func GetStructureField(field reflect.Value) interface{} {
if !field.IsValid() {
return nil
}
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). //nolint:gosec // this conversion is is between types recommended by Go https://cs.opensource.google/go/go/+/master:src/reflect/value.go;l=2445
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). //nolint:gosec // this conversion is between types recommended by Go https://cs.opensource.google/go/go/+/master:src/reflect/value.go;l=2445
Elem().
Interface()
}
Expand All @@ -31,7 +31,7 @@ func SetStructureField(field reflect.Value, value interface{}) {
if !field.IsValid() {
return
}
reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). //nolint:gosec // this conversion is is between types recommended by Go https://cs.opensource.google/go/go/+/master:src/reflect/value.go;l=2445
reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). //nolint:gosec // this conversion is between types recommended by Go https://cs.opensource.google/go/go/+/master:src/reflect/value.go;l=2445
Elem().
Set(reflect.ValueOf(value))
}
Expand Down
3 changes: 2 additions & 1 deletion utils/retry/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/go-logr/logr"

"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/safecast"
)

// RetryIf will retry fn when the value returned from retryConditionFn is true
Expand Down Expand Up @@ -39,7 +40,7 @@ func RetryIf(ctx context.Context, logger logr.Logger, retryPolicy *RetryPolicyCo
retry.MaxDelay(retryPolicy.RetryWaitMax),
retry.MaxJitter(25*time.Millisecond),
retry.DelayType(retryType),
retry.Attempts(uint(retryPolicy.RetryMax)), //nolint:gosec // in normal use this will have had Validate() called which enforces that the minimum number of RetryMax is 0 so it won't overflow
retry.Attempts(safecast.ToUint(retryPolicy.RetryMax)),
retry.RetryIf(retryConditionFn),
retry.LastErrorOnly(true),
retry.Context(ctx),
Expand Down
19 changes: 19 additions & 0 deletions utils/safecast/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Safecast

the purpose of this utilities is to perform safe number conversion in go similarly to [go-safecast](https://github.com/ccoVeille/go-safecast) from which they are inspired from.
It should help tackling gosec [G115 rule](https://github.com/securego/gosec/pull/1149)

G115: Potential overflow when converting between integer types.

and [CWE-190](https://cwe.mitre.org/data/definitions/190.html)


infinite loop
access to wrong resource by id
grant access to someone who exhausted their quota

Contrary to `go-safecast` no error is returned when attempting casting and the MAX or MIN value of the type is returned instead if the value is beyond the allowed window.
For instance, `toInt8(255)-> 127` and `toInt8(-255)-> -128`



35 changes: 35 additions & 0 deletions utils/safecast/boundary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package safecast

func greaterThanUpperBoundary[C1 IConvertable, C2 IConvertable](value C1, upperBoundary C2) (greater bool) {
if value <= 0 {
return
}

switch f := any(value).(type) {
case float64:
greater = f >= float64(upperBoundary)
case float32:
greater = float64(f) >= float64(upperBoundary)
default:
// for all other integer types, it fits in an uint64 without overflow as we know value is positive.
greater = uint64(value) > uint64(upperBoundary)
}

return
}

func lessThanLowerBoundary[T IConvertable, T2 IConvertable](value T, boundary T2) (lower bool) {
if value >= 0 {
return
}

switch f := any(value).(type) {
case float64:
lower = f <= float64(boundary)
case float32:
lower = float64(f) <= float64(boundary)
default:
lower = int64(value) < int64(boundary)
}
return
}
Loading

0 comments on commit 878f149

Please sign in to comment.