Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit fe6cf8c

Browse files
authored
Merge pull request #275 from owen-mc/incorrect-integer-conversion
Incorrect integer conversion
2 parents 08d9af1 + 951d597 commit fe6cf8c

25 files changed

+1189
-772
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ ql/src/go.dbscheme.stats: ql/src/go.dbscheme build/stats/src.stamp extractor
107107

108108
test: all build/testdb/check-upgrade-path
109109
codeql test run ql/test --search-path .
110+
env GOARCH=386 codeql$(EXE) test run ql/test/query-tests/Security/CWE-681 --search-path .
110111
cd extractor; go test -mod=vendor ./... | grep -vF "[no test files]"
111112

112113
.PHONY: build/testdb/check-upgrade-path
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Query "Incorrect integer conversion" (`go/incorrect-integer-conversion`) is promoted from experimental status. This checks for parsing a string to an integer and then assigning it to an integer type of a smaller bit size.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
If a string is parsed into an int using <code>strconv.Atoi</code>, and subsequently that int
8+
is converted into another integer type of a smaller size, the result can produce unexpected values.
9+
</p>
10+
<p>
11+
This also applies to the results of <code>strconv.ParseInt</code> and <code>strconv.ParseUint</code> when
12+
the specified size is larger than the size of the type that number is converted to.
13+
</p>
14+
</overview>
15+
16+
<recommendation>
17+
<p>
18+
If you need to parse integer values with specific bit sizes, avoid <code>strconv.Atoi</code>, and instead
19+
use <code>strconv.ParseInt</code> or <code>strconv.ParseUint</code>, which also allow specifying the
20+
bit size.
21+
</p>
22+
<p>
23+
When using those functions, be careful to not convert the result to another type with a smaller bit size than
24+
the bit size you specified when parsing the number.
25+
</p>
26+
<p>
27+
If this is not possible, then add upper (and lower) bound checks specific to each type and
28+
bit size (you can find the minimum and maximum value for each type in the <code>math</code> package).
29+
</p>
30+
</recommendation>
31+
32+
<example>
33+
<p>
34+
In the first example, assume that an input string is passed to <code>parseAllocateBad1</code> function,
35+
parsed by <code>strconv.Atoi</code>, and then converted into an <code>int32</code> type:
36+
</p>
37+
<sample src="IncorrectIntegerConversion.go"/>
38+
<p>
39+
The bounds are not checked, so this means that if the provided number is greater than the maximum value of type <code>int32</code>,
40+
the resulting value from the conversion will be different from the actual provided value.
41+
</p>
42+
<p>
43+
To avoid unexpected values, you should either use the other functions provided by the <code>strconv</code>
44+
package to parse the specific types and bit sizes as shown in the
45+
<code>parseAllocateGood2</code> function; or check bounds as in the <code>parseAllocateGood1</code>
46+
function.
47+
</p>
48+
<sample src="IncorrectIntegerConversionGood.go"/>
49+
</example>
50+
51+
<example>
52+
<p>
53+
In the second example, assume that an input string is passed to <code>parseAllocateBad2</code> function,
54+
parsed by <code>strconv.ParseInt</code> with a bit size set to 64, and then converted into an <code>int32</code> type:
55+
</p>
56+
<sample src="IncorrectIntegerConversion.go"/>
57+
<p>
58+
If the provided number is greater than the maximum value of type <code>int32</code>, the resulting value from the conversion will be
59+
different from the actual provided value.
60+
</p>
61+
<p>
62+
To avoid unexpected values, you should specify the correct bit size as in <code>parseAllocateGood3</code>;
63+
or check bounds before making the conversion as in <code>parseAllocateGood4</code>.
64+
</p>
65+
<sample src="IncorrectIntegerConversionGood.go"/>
66+
</example>
67+
<references>
68+
<li>Wikipedia <a href="https://en.wikipedia.org/wiki/Integer_overflow">Integer overflow</a>.</li>
69+
<li>Go language specification <a href="https://golang.org/ref/spec#Integer_overflow">Integer overflow</a>.</li>
70+
<li>Documentation for <a href="https://golang.org/pkg/strconv/#Atoi"><code>strconv.Atoi</code></a>.</li>
71+
<li>Documentation for <a href="https://golang.org/pkg/strconv/#ParseInt"><code>strconv.ParseInt</code></a>.</li>
72+
<li>Documentation for <a href="https://golang.org/pkg/strconv/#ParseUint"><code>strconv.ParseUint</code></a>.</li>
73+
</references>
74+
</qhelp>
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* @name Incorrect conversion between integer types
3+
* @description Converting the result of `strconv.Atoi`, `strconv.ParseInt`,
4+
* and `strconv.ParseUint` to integer types of smaller bit size
5+
* can produce unexpected values.
6+
* @kind path-problem
7+
* @problem.severity warning
8+
* @id go/incorrect-integer-conversion
9+
* @tags security
10+
* external/cwe/cwe-190
11+
* external/cwe/cwe-681
12+
* @precision very-high
13+
*/
14+
15+
import go
16+
import DataFlow::PathGraph
17+
18+
/**
19+
* Gets the maximum value of an integer (signed if `isSigned`
20+
* is true, unsigned otherwise) with `bitSize` bits.
21+
*/
22+
float getMaxIntValue(int bitSize, boolean isSigned) {
23+
bitSize in [8, 16, 32] and
24+
(
25+
isSigned = true and result = 2.pow(bitSize - 1) - 1
26+
or
27+
isSigned = false and result = 2.pow(bitSize) - 1
28+
)
29+
}
30+
31+
/**
32+
* Get the size of `int` or `uint` in `file`, or 0 if it is
33+
* architecture-specific.
34+
*/
35+
int getIntTypeBitSize(File file) {
36+
file.constrainsIntBitSize(result)
37+
or
38+
not file.constrainsIntBitSize(_) and
39+
result = 0
40+
}
41+
42+
/**
43+
* Holds if converting from an integer types with size `sourceBitSize` to
44+
* one with size `sinkBitSize` can produce unexpected values, where 0 means
45+
* architecture-dependent.
46+
*
47+
* Architecture-dependent bit sizes can be 32 or 64. To catch flows that
48+
* only manifest on 64-bit architectures we consider an
49+
* architecture-dependent source bit size to be 64. To catch flows that
50+
* only happen on 32-bit architectures we consider an
51+
* architecture-dependent sink bit size to be 32. We exclude the case where
52+
* both source and sink have architecture-dependent bit sizes.
53+
*/
54+
private predicate isIncorrectIntegerConversion(int sourceBitSize, int sinkBitSize) {
55+
sourceBitSize in [16, 32, 64] and
56+
sinkBitSize in [8, 16, 32] and
57+
sourceBitSize > sinkBitSize
58+
or
59+
// Treat `sourceBitSize = 0` like `sourceBitSize = 64`, and exclude `sinkBitSize = 0`
60+
sourceBitSize = 0 and
61+
sinkBitSize in [8, 16, 32]
62+
or
63+
// Treat `sinkBitSize = 0` like `sinkBitSize = 32`, and exclude `sourceBitSize = 0`
64+
sourceBitSize = 64 and
65+
sinkBitSize = 0
66+
}
67+
68+
/**
69+
* A taint-tracking configuration for reasoning about when an integer
70+
* obtained from parsing a string flows to a type conversion to a smaller
71+
* integer types, which could cause unexpected values.
72+
*/
73+
class ConversionWithoutBoundsCheckConfig extends TaintTracking::Configuration {
74+
boolean sourceIsSigned;
75+
int sourceBitSize;
76+
int sinkBitSize;
77+
78+
ConversionWithoutBoundsCheckConfig() {
79+
sourceIsSigned in [true, false] and
80+
isIncorrectIntegerConversion(sourceBitSize, sinkBitSize) and
81+
this = "ConversionWithoutBoundsCheckConfig" + sourceBitSize + sourceIsSigned + sinkBitSize
82+
}
83+
84+
/** Gets the bit size of the source. */
85+
int getSourceBitSize() { result = sourceBitSize }
86+
87+
override predicate isSource(DataFlow::Node source) {
88+
exists(
89+
DataFlow::CallNode c, IntegerParser::Range ip, int apparentBitSize, int effectiveBitSize
90+
|
91+
c.getTarget() = ip and source = c.getResult(0)
92+
|
93+
(
94+
if ip.getResultType(0) instanceof SignedIntegerType
95+
then sourceIsSigned = true
96+
else sourceIsSigned = false
97+
) and
98+
(
99+
apparentBitSize = ip.getTargetBitSize()
100+
or
101+
// If we are reading a variable, check if it is
102+
// `strconv.IntSize`, and use 0 if it is.
103+
exists(DataFlow::Node rawBitSize | rawBitSize = ip.getTargetBitSizeInput().getNode(c) |
104+
if rawBitSize = any(StrConv::IntSize intSize).getARead()
105+
then apparentBitSize = 0
106+
else apparentBitSize = rawBitSize.getIntValue()
107+
)
108+
) and
109+
(
110+
if apparentBitSize = 0
111+
then effectiveBitSize = getIntTypeBitSize(source.getFile())
112+
else effectiveBitSize = apparentBitSize
113+
) and
114+
// `effectiveBitSize` could be any value between 0 and 64, but we
115+
// can round it up to the nearest size of an integer type without
116+
// changing behaviour.
117+
sourceBitSize = min(int b | b in [0, 8, 16, 32, 64] and b >= effectiveBitSize)
118+
)
119+
}
120+
121+
/**
122+
* Holds if `sink` is a typecast to an integer type with size `bitSize` (where
123+
* 0 represents architecture-dependent) and the expression being typecast is
124+
* not also in a right-shift expression. We allow this case because it is
125+
* a common pattern to serialise `byte(v)`, `byte(v >> 8)`, and so on.
126+
*/
127+
predicate isSink(DataFlow::TypeCastNode sink, int bitSize) {
128+
exists(IntegerType integerType | sink.getType().getUnderlyingType() = integerType |
129+
bitSize = integerType.getSize()
130+
or
131+
not exists(integerType.getSize()) and
132+
bitSize = getIntTypeBitSize(sink.getFile())
133+
) and
134+
not exists(ShrExpr shrExpr |
135+
shrExpr.getLeftOperand().getGlobalValueNumber() =
136+
sink.getOperand().asExpr().getGlobalValueNumber() or
137+
shrExpr.getLeftOperand().(AndExpr).getAnOperand().getGlobalValueNumber() =
138+
sink.getOperand().asExpr().getGlobalValueNumber()
139+
)
140+
}
141+
142+
override predicate isSink(DataFlow::Node sink) { isSink(sink, sinkBitSize) }
143+
144+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
145+
// To catch flows that only happen on 32-bit architectures we
146+
// consider an architecture-dependent sink bit size to be 32.
147+
exists(int bitSize | if sinkBitSize != 0 then bitSize = sinkBitSize else bitSize = 32 |
148+
guard.(UpperBoundCheckGuard).getBound() <= getMaxIntValue(bitSize, sourceIsSigned)
149+
)
150+
}
151+
152+
override predicate isSanitizerOut(DataFlow::Node node) {
153+
exists(int bitSize | isIncorrectIntegerConversion(sourceBitSize, bitSize) |
154+
isSink(node, bitSize)
155+
)
156+
}
157+
}
158+
159+
/** An upper bound check that compares a variable to a constant value. */
160+
class UpperBoundCheckGuard extends DataFlow::BarrierGuard, DataFlow::RelationalComparisonNode {
161+
UpperBoundCheckGuard() { count(expr.getAnOperand().getIntValue()) = 1 }
162+
163+
/**
164+
* Gets the constant value which this upper bound check ensures the
165+
* other value is less than or equal to.
166+
*/
167+
float getBound() {
168+
exists(int strictnessOffset |
169+
if expr.isStrict() then strictnessOffset = 1 else strictnessOffset = 0
170+
|
171+
result = expr.getAnOperand().getExactValue().toFloat() - strictnessOffset
172+
)
173+
}
174+
175+
override predicate checks(Expr e, boolean branch) {
176+
this.leq(branch, DataFlow::exprNode(e), _, _) and
177+
not exists(e.getIntValue())
178+
}
179+
}
180+
181+
/** Gets a string describing the size of the integer parsed. */
182+
string describeBitSize(int bitSize, int intTypeBitSize) {
183+
intTypeBitSize in [0, 32, 64] and
184+
if bitSize != 0
185+
then bitSize in [8, 16, 32, 64] and result = "a " + bitSize + "-bit integer"
186+
else
187+
if intTypeBitSize = 0
188+
then result = "an integer with architecture-dependent bit size"
189+
else
190+
result =
191+
"a number with architecture-dependent bit-width, which is constrained to be " +
192+
intTypeBitSize + "-bit by build constraints,"
193+
}
194+
195+
from
196+
DataFlow::PathNode source, DataFlow::PathNode sink, ConversionWithoutBoundsCheckConfig cfg,
197+
DataFlow::CallNode call
198+
where cfg.hasFlowPath(source, sink) and call.getResult(0) = source.getNode()
199+
select sink.getNode(), source, sink,
200+
"Incorrect conversion of " +
201+
describeBitSize(cfg.getSourceBitSize(), getIntTypeBitSize(source.getNode().getFile())) +
202+
" from $@ to a lower bit size type " + sink.getNode().getType().getUnderlyingType().getName() +
203+
" without an upper bound check.", source, call.getTarget().getQualifiedName()

ql/src/experimental/CWE-681/IncorrectNumericConversionGood.go renamed to ql/src/Security/CWE-681/IncorrectIntegerConversionGood.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func parseAllocateGood1(desired string) int32 {
1616
if err != nil {
1717
return DefaultAllocate
1818
}
19-
// GOOD: check for lower and uppper bounds
19+
// GOOD: check for lower and upper bounds
2020
if parsed > 0 && parsed <= math.MaxInt32 {
2121
return int32(parsed)
2222
}

ql/src/experimental/CWE-681/IncorrectNumericConversion.qhelp

Lines changed: 0 additions & 65 deletions
This file was deleted.

0 commit comments

Comments
 (0)