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

Add a new rule to detect integer overflow when converting between integer types #1149

Merged
merged 5 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ _testmain.go

# SBOMs generated during CI
/bom.json
1
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ linters:
- govet
- importas
- ineffassign
- megacheck
- misspell
- nakedret
- nolintlint
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ directory you can supply `./...` as the input argument.
- G112: Potential slowloris attack
- G113: Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772)
- G114: Use of net/http serve function that has no support for setting timeouts
- G115: Potential integer overflow when converting between integer types
- G201: SQL query construction using format string
- G202: SQL query construction using string concatenation
- G203: Use of unescaped data in HTML templates
Expand Down
135 changes: 135 additions & 0 deletions analyzers/conversion_overflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// (c) Copyright gosec's authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package analyzers

import (
"fmt"
"regexp"
"strconv"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/buildssa"
"golang.org/x/tools/go/ssa"

"github.com/securego/gosec/v2/issue"
)

func newConversionOverflowAnalyzer(id string, description string) *analysis.Analyzer {
return &analysis.Analyzer{
Name: id,
Doc: description,
Run: runConversionOverflow,
Requires: []*analysis.Analyzer{buildssa.Analyzer},
}
}

func runConversionOverflow(pass *analysis.Pass) (interface{}, error) {
ssaResult, err := getSSAResult(pass)
if err != nil {
return nil, fmt.Errorf("building ssa representation: %w", err)
}

issues := []*issue.Issue{}
for _, mcall := range ssaResult.SSA.SrcFuncs {
for _, block := range mcall.DomPreorder() {
for _, instr := range block.Instrs {
switch instr := instr.(type) {
case *ssa.Convert:
src := instr.X.Type().String()
dst := instr.Type().String()
if isIntOverflow(src, dst) {
issue := newIssue(pass.Analyzer.Name,
fmt.Sprintf("integer overflow conversion %s -> %s", src, dst),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have missed something about the implementation, but I would like to suggest this

Suggested change
fmt.Sprintf("integer overflow conversion %s -> %s", src, dst),
fmt.Sprintf("possible integer overflow conversion %s -> %s", src, dst),

It might not happen depending on people code.

It's better to report a possible error, than affirming there is one. People would argue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccojocar what do you think about the changes I suggested?

Do you agree with them?

Would you change it? Or do you want me to open a PR for them?

pass.Fset,
instr.Pos(),
issue.High,
issue.Medium,
)
issues = append(issues, issue)
}
}
}
}
}

if len(issues) > 0 {
return issues, nil
}
return nil, nil
}

type integer struct {
signed bool
size int
}

func parseIntType(intType string) (integer, error) {
re := regexp.MustCompile(`(?P<type>u?int)(?P<size>\d{2})?`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regexp will be computed each time parseInt will be called, it should be moved out the function

matches := re.FindStringSubmatch(intType)
if matches == nil {
return integer{}, fmt.Errorf("no integer type match found for %s", intType)
}

it := matches[re.SubexpIndex("type")]
is := matches[re.SubexpIndex("size")]
Comment on lines +85 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add something like this

Suggested change
it := matches[re.SubexpIndex("type")]
is := matches[re.SubexpIndex("size")]
// int64 => int 64, uint8 => uint 8
it := matches[re.SubexpIndex("type")]
is := matches[re.SubexpIndex("size")]


signed := false
if it == "int" {
signed = true
}

// use default system int type in case size is not present in the type
intSize := strconv.IntSize
if is != "" {
var err error
intSize, err = strconv.Atoi(is)
if err != nil {
return integer{}, fmt.Errorf("failed to parse the integer type size: %w", err)
}
}

return integer{signed: signed, size: intSize}, nil
}

func isIntOverflow(src string, dst string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this

Suggested change
func isIntOverflow(src string, dst string) bool {
func couldIntOverflow(src string, dst string) bool {

srcInt, err := parseIntType(src)
if err != nil {
return false
}

dstInt, err := parseIntType(dst)
if err != nil {
return false
}

// converting uint to int of the same size or smaller might lead to overflow
if !srcInt.signed && dstInt.signed && dstInt.size <= srcInt.size {
return true
}
// converting uint to unit of a smaller size might lead to overflow
if !srcInt.signed && !dstInt.signed && dstInt.size < srcInt.size {
return true
}
// converting int to int of a smaller size might lead to overflow
if srcInt.signed && dstInt.signed && dstInt.size < srcInt.size {
return true
}
// converting int to uint of a smaller size might lead to overflow
if srcInt.signed && !dstInt.signed && dstInt.size < srcInt.size && srcInt.size-dstInt.size > 8 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 8 is magical number here

return true
}

return false
}
1 change: 1 addition & 0 deletions analyzers/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type SSAAnalyzerResult struct {
// BuildDefaultAnalyzers returns the default list of analyzers
func BuildDefaultAnalyzers() []*analysis.Analyzer {
return []*analysis.Analyzer{
newConversionOverflowAnalyzer("G115", "Type conversion which leads to integer overflow"),
newSliceBoundsAnalyzer("G602", "Possible slice bounds out of range"),
}
}
Expand Down
4 changes: 4 additions & 0 deletions rules/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ var _ = Describe("gosec rules", func() {
runner("G114", testutils.SampleCodeG114)
})

It("should detect integer conversion overflow", func() {
runner("G115", testutils.SampleCodeG115)
})

It("should detect sql injection via format strings", func() {
runner("G201", testutils.SampleCodeG201)
})
Expand Down
2 changes: 1 addition & 1 deletion testutils/g103_samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
intPtr = (*int)(unsafe.Pointer(addressHolder))
fmt.Printf("\nintPtr=%p, *intPtr=%d.\n\n", intPtr, *intPtr)
}
`}, 2, gosec.NewConfig()},
`}, 3, gosec.NewConfig()},
{[]string{`
package main

Expand Down
10 changes: 5 additions & 5 deletions testutils/g109_samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func main() {
value := int32(bigValue)
fmt.Println(value)
}
`}, 1, gosec.NewConfig()},
`}, 2, gosec.NewConfig()},
{[]string{`
package main

Expand All @@ -38,7 +38,7 @@ func main() {
fmt.Println(bigValue)
}
}
`}, 1, gosec.NewConfig()},
`}, 2, gosec.NewConfig()},
{[]string{`
package main

Expand Down Expand Up @@ -74,7 +74,7 @@ func main() {

func test() {
bigValue := 30
value := int32(bigValue)
value := int64(bigValue)
fmt.Println(value)
}
`}, 0, gosec.NewConfig()},
Expand All @@ -92,7 +92,7 @@ func main() {
value, _ := strconv.Atoi("2147483648")
fmt.Println(value)
}
v := int32(value)
v := int64(value)
fmt.Println(v)
}
`}, 0, gosec.NewConfig()},
Expand All @@ -105,7 +105,7 @@ import (
)
func main() {
a, err := strconv.Atoi("a")
b := int32(a) //#nosec G109
b := int64(a) //#nosec G109
fmt.Println(b, err)
}
`}, 0, gosec.NewConfig()},
Expand Down
118 changes: 118 additions & 0 deletions testutils/g115_samples.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package testutils

import "github.com/securego/gosec/v2"

var SampleCodeG115 = []CodeSample{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried these tests are only focusing on things that could be reported.

I would expect to be added to cover conversion of things that should pass.

They are likely the ones to report false positive if something is invalid in the current implementation, or if a later refactoring screw everything

{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a uint32 = math.MaxUint32
b := int32(a)
fmt.Println(b)
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a uint16 = math.MaxUint16
b := int32(a)
fmt.Println(b)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a uint32 = math.MaxUint32
b := uint16(a)
fmt.Println(b)
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a int32 = math.MaxInt32
b := int16(a)
fmt.Println(b)
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a int16 = math.MaxInt16
b := int32(a)
fmt.Println(b)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a int32 = math.MaxInt32
b := uint32(a)
fmt.Println(b)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a uint = math.MaxUint
b := int16(a)
fmt.Println(b)
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main

import (
"fmt"
"math"
)

func main() {
var a uint = math.MaxUint
b := int64(a)
fmt.Println(b)
}
`}, 1, gosec.NewConfig()},
}
Loading