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

refactor: disallow non alphanumeric symbols #945

Merged
merged 3 commits into from
Jun 18, 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
6 changes: 3 additions & 3 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
bytecode_hash = "none"
evm_version = "shanghai"
fs_permissions = [
{ access = "read", path = "./out-optimized"},
{ access = "read", path = "package.json"},
{ access = "read-write", path = "./benchmark/results"}
{ access = "read", path = "./out-optimized" },
{ access = "read", path = "package.json" },
{ access = "read-write", path = "./benchmark/results" },
]
gas_reports = [
"SablierV2LockupDynamic",
Expand Down
2 changes: 1 addition & 1 deletion precompiles/Precompiles.sol

Large diffs are not rendered by default.

29 changes: 27 additions & 2 deletions src/SablierV2NFTDescriptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,28 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor {
return string.concat("Sablier V2 ", sablierModel, " #", streamId);
}

/// @notice Checks whether the provided string contains only alphanumeric characters and spaces.
/// @dev Note that this returns true for empty strings, but it is not a security concern.
function isAlphanumeric(string memory str) internal pure returns (bool) {
// Convert the string to bytes to iterate over its characters.
bytes memory b = bytes(str);

uint256 length = b.length;
for (uint256 i = 0; i < length; ++i) {
bytes1 char = b[i];

// Check if it's a space or an alphanumeric character.
bool isSpace = char == 0x20; // space
bool isDigit = char >= 0x30 && char <= 0x39; // 0-9
bool isUppercaseLetter = char >= 0x41 && char <= 0x5A; // A-Z
bool isLowercaseLetter = char >= 0x61 && char <= 0x7A; // a-z
if (!(isSpace || isDigit || isUppercaseLetter || isLowercaseLetter)) {
return false;
}
}
return true;
}

/// @notice Maps ERC-721 symbols to human-readable model names.
/// @dev Reverts if the symbol is unknown.
function mapSymbol(IERC721Metadata sablier) internal view returns (string memory) {
Expand Down Expand Up @@ -341,11 +363,14 @@ contract SablierV2NFTDescriptor is ISablierV2NFTDescriptor {

string memory symbol = abi.decode(returnData, (string));

// The length check is a precautionary measure to help mitigate potential security threats from malicious assets
// injecting scripts in the symbol string.
// Check if the symbol is too long or contains non-alphanumeric characters, this measure helps mitigate
// potential security threats from malicious assets injecting scripts in the symbol string.
if (bytes(symbol).length > 30) {
return "Long Symbol";
} else {
if (!isAlphanumeric(symbol)) {
return "Non-Alphanumeric Symbol";
}
return symbol;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22 <0.9.0;

import { NFTDescriptor_Integration_Concrete_Test } from "./NFTDescriptor.t.sol";
import { NFTDescriptor_Integration_Shared_Test } from "../../shared/nft-descriptor/NFTDescriptor.t.sol";

contract GenerateAccentColor_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test {
contract GenerateAccentColor_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test {
function test_GenerateAccentColor() external view {
// Passing a dummy contract instead of a real Sablier contract to make this test easy to maintain.
string memory actualColor = nftDescriptorMock.generateAccentColor_({ sablier: address(noop), streamId: 1337 });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22 <0.9.0;

import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol";

contract IsAlphanumeric_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test {
function test_IsAlphanumeric_EmptyString() external view {
string memory symbol = "";
bool result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");
}

modifier whenNotEmptyString() {
_;
}

function test_IsAlphanumeric_ContainsNonAlphanumericCharacters() external view whenNotEmptyString {
string memory symbol = "<foo/>";
bool result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo/";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo\\";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo%";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo&";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo(";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo)";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo\"";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo'";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo`";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo;";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");

symbol = "foo%20"; // URL-encoded empty space
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertFalse(result, "isAlphanumeric");
}

modifier whenOnlyAlphanumericCharacters() {
_;
}

function test_IsAlphanumeric() external view whenNotEmptyString whenOnlyAlphanumericCharacters {
string memory symbol = "foo";
bool result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");

symbol = "Foo";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");

symbol = "Foo ";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");

symbol = "Foo Bar";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");

symbol = " ";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");

symbol = "foo01234";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");

symbol = "123456789";
result = nftDescriptorMock.isAlphanumeric_(symbol);
assertTrue(result, "isAlphanumeric");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
isAlphanumeric.t.sol
├── when the symbol is an empty string
│ └── it should return true
└── when the symbol is not an empty string
├── given the symbol does not contain only alphanumeric characters
│ └── it should return false
└── given the symbol contains only alphanumeric characters
└── it should return true
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { MockERC721 } from "forge-std/src/mocks/MockERC721.sol";

import { Errors } from "src/libraries/Errors.sol";

import { NFTDescriptor_Integration_Concrete_Test } from "../NFTDescriptor.t.sol";
import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol";

contract MapSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test {
contract MapSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test {
function test_RevertGiven_UnknownNFT() external {
MockERC721 nft = new MockERC721();
nft.initialize("Foo", "FOO");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22 <0.9.0;

import { NFTDescriptor_Integration_Concrete_Test } from "../NFTDescriptor.t.sol";
import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol";

contract SafeAssetDecimals_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test {
contract SafeAssetDecimals_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test {
function test_SafeAssetDecimals_EOA() external view {
address eoa = vm.addr({ privateKey: 1 });
uint8 actualDecimals = nftDescriptorMock.safeAssetDecimals_(address(eoa));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ pragma solidity >=0.8.22 <0.9.0;

import { ERC20Mock } from "../../../../mocks/erc20/ERC20Mock.sol";
import { ERC20Bytes32 } from "../../../../mocks/erc20/ERC20Bytes32.sol";
import { NFTDescriptor_Integration_Concrete_Test } from "../NFTDescriptor.t.sol";
import { NFTDescriptor_Integration_Shared_Test } from "../../../shared/nft-descriptor/NFTDescriptor.t.sol";

contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Concrete_Test {
contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_Shared_Test {
function test_SafeAssetSymbol_EOA() external view {
address eoa = vm.addr({ privateKey: 1 });
string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(eoa));
Expand Down Expand Up @@ -48,7 +48,25 @@ contract SafeAssetSymbol_Integration_Concrete_Test is NFTDescriptor_Integration_
_;
}

function test_SafeAssetSymbol() external view whenERC20Contract givenSymbolString givenSymbolNotLong {
function test_SafeAssetSymbol_NonAlphanumeric() external whenERC20Contract givenSymbolString givenSymbolNotLong {
ERC20Mock asset = new ERC20Mock({ name: "Token", symbol: "<svg/onload=alert(\"xss\")>" });
string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(asset));
string memory expectedSymbol = "Non-Alphanumeric Symbol";
assertEq(actualSymbol, expectedSymbol, "symbol");
}

modifier givenSymbolAlphanumeric() {
_;
}

function test_SafeAssetSymbol()
external
view
whenERC20Contract
givenSymbolString
givenSymbolNotLong
givenSymbolAlphanumeric
{
string memory actualSymbol = nftDescriptorMock.safeAssetSymbol_(address(dai));
string memory expectedSymbol = dai.symbol();
assertEq(actualSymbol, expectedSymbol, "symbol");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ safeAssetSymbol.t.sol
├── given the symbol is longer than 30 characters
│ └── it should return a hard-coded values
└── given the symbol is not longer than 30 characters
└── it should return the correct symbol value
├── given the symbol contains non-alphanumeric characters
│ └── it should return a hard-coded value
└── given the symbol contains only alphanumeric characters
└── it should return the correct symbol value
46 changes: 46 additions & 0 deletions test/integration/fuzz/nft-descriptor/isAlphanumeric.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.22 <0.9.0;

import { NFTDescriptor_Integration_Shared_Test } from "../../shared/nft-descriptor/NFTDescriptor.t.sol";

contract IsAlphanumeric_Integration_Fuzz_Test is NFTDescriptor_Integration_Shared_Test {
bytes1 internal constant SPACE = 0x20; // ASCII 32
bytes1 internal constant ZERO = 0x30; // ASCII 48
bytes1 internal constant NINE = 0x39; // ASCII 57
bytes1 internal constant A = 0x41; // ASCII 65
bytes1 internal constant Z = 0x5A; // ASCII 90
bytes1 internal constant a = 0x61; // ASCII 97
bytes1 internal constant z = 0x7A; // ASCII 122

modifier whenNotEmptyString() {
_;
}

/// @dev Given enough fuzz runs, all the following scenarios will be fuzzed:
///
/// - String with only alphanumerical characters
/// - String with only non-alphanumerical characters
/// - String with both alphanumerical and non-alphanumerical characters
function testFuzz_IsAlphanumeric(string memory symbol) external view whenNotEmptyString {
bytes memory b = bytes(symbol);
uint256 length = b.length;
bool expectedResult = true;
for (uint256 i = 0; i < length; ++i) {
bytes1 char = b[i];
if (!isAlphanumericChar(char)) {
expectedResult = false;
break;
}
}
bool actualResult = nftDescriptorMock.isAlphanumeric_(symbol);
assertEq(actualResult, expectedResult, "isAlphanumeric");
}

function isAlphanumericChar(bytes1 char) internal pure returns (bool) {
bool isSpace = char == SPACE;
bool isDigit = char >= ZERO && char <= NINE;
bool isUppercaseLetter = char >= A && char <= Z;
bool isLowercaseLetter = char >= a && char <= z;
return isSpace || isDigit || isUppercaseLetter || isLowercaseLetter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity >=0.8.22 <0.9.0;
import { Integration_Test } from "../../Integration.t.sol";
import { NFTDescriptorMock } from "../../../mocks/NFTDescriptorMock.sol";

abstract contract NFTDescriptor_Integration_Concrete_Test is Integration_Test {
abstract contract NFTDescriptor_Integration_Shared_Test is Integration_Test {
NFTDescriptorMock internal nftDescriptorMock;

function setUp() public virtual override {
Expand Down
4 changes: 4 additions & 0 deletions test/mocks/NFTDescriptorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ contract NFTDescriptorMock is SablierV2NFTDescriptor {
return SVGElements.hourglass(status);
}

function isAlphanumeric_(string memory symbol) external pure returns (bool) {
return isAlphanumeric(symbol);
}

function mapSymbol_(IERC721Metadata nft) external view returns (string memory) {
return mapSymbol(nft);
}
Expand Down