Skip to content

Commit fa995ef

Browse files
DarkLord017Amxxarr00
authored
Add espaceJSON to String.sol (#5508)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
1 parent 3882a09 commit fa995ef

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

.changeset/nice-cherries-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Strings`: Add `espaceJSON` that escapes special characters in JSON strings.

contracts/utils/Strings.sol

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ library Strings {
1515

1616
bytes16 private constant HEX_DIGITS = "0123456789abcdef";
1717
uint8 private constant ADDRESS_LENGTH = 20;
18+
uint256 private constant SPECIAL_CHARS_LOOKUP =
19+
(1 << 0x08) | // backspace
20+
(1 << 0x09) | // tab
21+
(1 << 0x0a) | // newline
22+
(1 << 0x0c) | // form feed
23+
(1 << 0x0d) | // carriage return
24+
(1 << 0x22) | // double quote
25+
(1 << 0x5c); // backslash
1826

1927
/**
2028
* @dev The `value` string doesn't fit in the specified `length`.
@@ -426,6 +434,43 @@ library Strings {
426434
return value;
427435
}
428436

437+
/**
438+
* @dev Escape special characters in JSON strings. This can be useful to prevent JSON injection in NFT metadata.
439+
*
440+
* WARNING: This function should only be used in double quoted JSON strings. Single quotes are not escaped.
441+
*/
442+
function escapeJSON(string memory input) internal pure returns (string memory) {
443+
bytes memory buffer = bytes(input);
444+
bytes memory output = new bytes(2 * buffer.length); // worst case scenario
445+
uint256 outputLength = 0;
446+
447+
for (uint256 i; i < buffer.length; ++i) {
448+
bytes1 char = bytes1(_unsafeReadBytesOffset(buffer, i));
449+
if (((SPECIAL_CHARS_LOOKUP & (1 << uint8(char))) != 0)) {
450+
output[outputLength++] = "\\";
451+
if (char == 0x08) output[outputLength++] = "b";
452+
else if (char == 0x09) output[outputLength++] = "t";
453+
else if (char == 0x0a) output[outputLength++] = "n";
454+
else if (char == 0x0c) output[outputLength++] = "f";
455+
else if (char == 0x0d) output[outputLength++] = "r";
456+
else if (char == 0x5c) output[outputLength++] = "\\";
457+
else if (char == 0x22) {
458+
// solhint-disable-next-line quotes
459+
output[outputLength++] = '"';
460+
}
461+
} else {
462+
output[outputLength++] = char;
463+
}
464+
}
465+
// write the actual length and deallocate unused memory
466+
assembly ("memory-safe") {
467+
mstore(output, outputLength)
468+
mstore(0x40, add(output, shl(5, shr(5, add(outputLength, 63)))))
469+
}
470+
471+
return string(output);
472+
}
473+
429474
/**
430475
* @dev Reads a bytes32 from a bytes array without bounds checking.
431476
*

test/utils/Strings.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,11 @@ describe('Strings', function () {
339339
}
340340
});
341341
});
342+
343+
describe('Escape JSON string', function () {
344+
for (const input of ['', 'a', '{"a":"b/c"}', 'a\tb\nc\\d"e\rf/g\fh\bi'])
345+
it(`escape ${JSON.stringify(input)}`, async function () {
346+
await expect(this.mock.$escapeJSON(input)).to.eventually.equal(JSON.stringify(input).slice(1, -1));
347+
});
348+
});
342349
});

0 commit comments

Comments
 (0)