Skip to content

Commit b3ce884

Browse files
ernestognwAmxx
andauthored
Refactor parseUint, parseInt and parseHexUint to check bounds (#5304)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent d11ed2f commit b3ce884

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
lines changed

contracts/utils/Strings.sol

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ library Strings {
158158
* NOTE: This function will revert if the result does not fit in a `uint256`.
159159
*/
160160
function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) {
161-
return tryParseUint(input, 0, bytes(input).length);
161+
return _tryParseUintUncheckedBounds(input, 0, bytes(input).length);
162162
}
163163

164164
/**
@@ -172,6 +172,18 @@ library Strings {
172172
uint256 begin,
173173
uint256 end
174174
) internal pure returns (bool success, uint256 value) {
175+
if (end > bytes(input).length || begin > end) return (false, 0);
176+
return _tryParseUintUncheckedBounds(input, begin, end);
177+
}
178+
179+
/**
180+
* @dev Variant of {tryParseUint} that does not check bounds and returns (true, 0) if they are invalid.
181+
*/
182+
function _tryParseUintUncheckedBounds(
183+
string memory input,
184+
uint256 begin,
185+
uint256 end
186+
) private pure returns (bool success, uint256 value) {
175187
bytes memory buffer = bytes(input);
176188

177189
uint256 result = 0;
@@ -216,7 +228,7 @@ library Strings {
216228
* NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`.
217229
*/
218230
function tryParseInt(string memory input) internal pure returns (bool success, int256 value) {
219-
return tryParseInt(input, 0, bytes(input).length);
231+
return _tryParseIntUncheckedBounds(input, 0, bytes(input).length);
220232
}
221233

222234
uint256 private constant ABS_MIN_INT256 = 2 ** 255;
@@ -232,10 +244,22 @@ library Strings {
232244
uint256 begin,
233245
uint256 end
234246
) internal pure returns (bool success, int256 value) {
247+
if (end > bytes(input).length || begin > end) return (false, 0);
248+
return _tryParseIntUncheckedBounds(input, begin, end);
249+
}
250+
251+
/**
252+
* @dev Variant of {tryParseInt} that does not check bounds and returns (true, 0) if they are invalid.
253+
*/
254+
function _tryParseIntUncheckedBounds(
255+
string memory input,
256+
uint256 begin,
257+
uint256 end
258+
) private pure returns (bool success, int256 value) {
235259
bytes memory buffer = bytes(input);
236260

237261
// Check presence of a negative sign.
238-
bytes1 sign = bytes1(_unsafeReadBytesOffset(buffer, begin));
262+
bytes1 sign = begin == end ? bytes1(0) : bytes1(_unsafeReadBytesOffset(buffer, begin)); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
239263
bool positiveSign = sign == bytes1("+");
240264
bool negativeSign = sign == bytes1("-");
241265
uint256 offset = (positiveSign || negativeSign).toUint();
@@ -280,7 +304,7 @@ library Strings {
280304
* NOTE: This function will revert if the result does not fit in a `uint256`.
281305
*/
282306
function tryParseHexUint(string memory input) internal pure returns (bool success, uint256 value) {
283-
return tryParseHexUint(input, 0, bytes(input).length);
307+
return _tryParseHexUintUncheckedBounds(input, 0, bytes(input).length);
284308
}
285309

286310
/**
@@ -294,10 +318,22 @@ library Strings {
294318
uint256 begin,
295319
uint256 end
296320
) internal pure returns (bool success, uint256 value) {
321+
if (end > bytes(input).length || begin > end) return (false, 0);
322+
return _tryParseHexUintUncheckedBounds(input, begin, end);
323+
}
324+
325+
/**
326+
* @dev Variant of {tryParseHexUint} that does not check bounds and returns (true, 0) if they are invalid.
327+
*/
328+
function _tryParseHexUintUncheckedBounds(
329+
string memory input,
330+
uint256 begin,
331+
uint256 end
332+
) private pure returns (bool success, uint256 value) {
297333
bytes memory buffer = bytes(input);
298334

299335
// skip 0x prefix if present
300-
bool hasPrefix = bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x");
336+
bool hasPrefix = (begin < end + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
301337
uint256 offset = hasPrefix.toUint() * 2;
302338

303339
uint256 result = 0;
@@ -355,12 +391,13 @@ library Strings {
355391
uint256 end
356392
) internal pure returns (bool success, address value) {
357393
// check that input is the correct length
358-
bool hasPrefix = bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x");
394+
bool hasPrefix = (begin < end + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
395+
359396
uint256 expectedLength = 40 + hasPrefix.toUint() * 2;
360397

361-
if (end - begin == expectedLength) {
398+
if (end - begin == expectedLength && end <= bytes(input).length) {
362399
// length guarantees that this does not overflow, and value is at most type(uint160).max
363-
(bool s, uint256 v) = tryParseHexUint(input, begin, end);
400+
(bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end);
364401
return (s, address(uint160(v)));
365402
} else {
366403
return (false, address(0));

test/utils/Strings.t.sol

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,27 @@ contract StringsTest is Test {
2424
function testParseChecksumHex(address value) external pure {
2525
assertEq(value, value.toChecksumHexString().parseAddress());
2626
}
27+
28+
function testTryParseHexUintExtendedEnd(string memory random) external pure {
29+
uint256 length = bytes(random).length;
30+
assembly ("memory-safe") {
31+
mstore(add(add(random, 0x20), length), 0x3030303030303030303030303030303030303030303030303030303030303030)
32+
}
33+
34+
(bool success, ) = random.tryParseHexUint(1, length + 1);
35+
assertFalse(success);
36+
}
37+
38+
function testTryParseAddressExtendedEnd(address random, uint256 begin) external pure {
39+
begin = bound(begin, 3, 43);
40+
string memory input = random.toHexString();
41+
uint256 length = bytes(input).length;
42+
43+
assembly ("memory-safe") {
44+
mstore(add(add(input, 0x20), length), 0x3030303030303030303030303030303030303030303030303030303030303030)
45+
}
46+
47+
(bool success, ) = input.tryParseAddress(begin, begin + 40);
48+
assertFalse(success);
49+
}
2750
}

test/utils/Strings.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ describe('Strings', function () {
240240
expect(await this.mock.$tryParseUint('1 000')).deep.equal([false, 0n]);
241241
});
242242

243+
it('parseUint invalid range', async function () {
244+
expect(this.mock.$parseUint('12', 3, 2)).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar');
245+
expect(await this.mock.$tryParseUint('12', 3, 2)).to.deep.equal([false, 0n]);
246+
});
247+
243248
it('parseInt overflow', async function () {
244249
await expect(this.mock.$parseInt((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic(
245250
PANIC_CODES.ARITHMETIC_OVERFLOW,
@@ -276,6 +281,11 @@ describe('Strings', function () {
276281
expect(await this.mock.$tryParseInt('1 000')).to.deep.equal([false, 0n]);
277282
});
278283

284+
it('parseInt invalid range', async function () {
285+
expect(this.mock.$parseInt('-12', 3, 2)).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar');
286+
expect(await this.mock.$tryParseInt('-12', 3, 2)).to.deep.equal([false, 0n]);
287+
});
288+
279289
it('parseHexUint overflow', async function () {
280290
await expect(this.mock.$parseHexUint((ethers.MaxUint256 + 1n).toString(16))).to.be.revertedWithPanic(
281291
PANIC_CODES.ARITHMETIC_OVERFLOW,
@@ -303,6 +313,11 @@ describe('Strings', function () {
303313
expect(await this.mock.$tryParseHexUint('1 000')).to.deep.equal([false, 0n]);
304314
});
305315

316+
it('parseHexUint invalid begin and end', async function () {
317+
expect(this.mock.$parseHexUint('0x', 3, 2)).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar');
318+
expect(await this.mock.$tryParseHexUint('0x', 3, 2)).to.deep.equal([false, 0n]);
319+
});
320+
306321
it('parseAddress invalid format', async function () {
307322
for (const addr of [
308323
'0x736a507fB2881d6bB62dcA54673CF5295dC07833', // valid

0 commit comments

Comments
 (0)