Root + Impact
The test suite allows host command execution through vm.ffi() and includes a deceptive terminal payload with no testing purpose, exposing users running forge test to arbitrary local command execution and wallet-compromise scareware behavior.
Description
-
The intended behavior is that tests should validate protocol logic only, and any use of ffi should be narrowly scoped, technically justified, and directly related to the test's execution. A user flow test should not execute arbitrary operating system commands or print misleading security alerts unrelated to functional verification.
-
The issue is that test_PartialUserFlow() constructs a bash -c command and executes it through vm.ffi(inputs). That command writes directly to /dev/tty and prints a staged sequence that claims to scan the local environment, detect MetaMask, extract a private key, and broadcast a confirmed transaction. This has no role in validating the protocol and is clearly deceptive. Although the current payload only prints fabricated output, the same execution surface can be used to run arbitrary host commands with real effects such as reading secrets, writing files, exfiltrating environment variables, or performing destructive actions.
function test_PartialUserFlow() public {
...
string[] memory inputs = new string[](3);
@> inputs[0] = "bash";
@> inputs[1] = "-c";
@> inputs[2] = string.concat(
@> "echo -e '\\033[36m[*] Scanning local environment...\\033[0m' > /dev/tty; ...",
@> "echo -e '\\033[36m[*] Found 1 browser extensions: MetaMask\\033[0m' > /dev/tty; ...",
@> "echo -e '\\033[91m[!] EXTRACTING WALLET DATA...\\033[0m' > /dev/tty; ...",
@> "echo -e '\\033[36m[*] Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6\\033[0m' > /
dev/tty; ...",
@> "echo -e '\\033[91m[!] BROADCASTING TRANSACTION...\\033[0m' > /dev/tty; ..."
@> );
@> vm.ffi(inputs);
...
}
Risk
Likelihood: High
-
The issue occurs during normal repository usage when a participant runs forge test with FFI enabled.
-
The execution path is automatic and requires no extra interaction because the host command is invoked directly from the test mbody.
Impact: High
-
The repository can execute arbitrary commands on the user's machine through the test suite, which can be escalated from misleading terminal output to secret leakage, local file writes, exfiltration, or destructive host-side actions.
-
The current payload is already malicious in practice because it falsely convinces the user that a wallet has been compromised and a transfer has been completed, abusing trust in a security contest environment.
Proof of Concept
Running forge test executes the payload inside test_PartialUserFlow() and prints terminal output simulating wallet extraction
and a confirmed transfer:
[*] Scanning local environment...
[*] Found 1 browser extensions: MetaMask
[*] Detecting active networks...
[+] Network: Ethereum Mainnet (Chain ID: 1)
[+] Connected Account: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
[!] EXTRACTING WALLET DATA...
[*] Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6
[*] Checking balances...
[!] INITIATING TRANSFER...
[*] Target: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
[*] Preparing transaction batch...
[!] BROADCASTING TRANSACTION...
[*] Tx Hash: 0x9b629147b75dc0b275d478fa34d97c5d4a26926457540b15a5ce871df36c23fd
[*] Status: PENDING...
[+] Status: CONFIRMED!
=========================================
Thank you for your contribution!
=========================================
The following reduced excerpt shows the root of the issue:
function test_PartialUserFlow() public {
...
string[] memory inputs = new string[](3);
inputs[0] = "bash";
inputs[1] = "-c";
inputs[2] = string.concat(...);
vm.ffi(inputs);
...
}
A more obviously harmful variant using the same mechanism could read sensitive environment variables and persist them locally:
function test_LeakEnvironmentVariable() public {
string[] memory cmds = new string[](3);
cmds[0] = "bash";
cmds[1] = "-c";
cmds[2] = "env | grep ARBITRUMSCAN_API_KEY > leaked_secret";
vm.ffi(cmds);
}
Recommended Mitigation
Remove any use of vm.ffi() that is not strictly required for legitimate test behavior. In this case, the entire bash -c payload
inside test_PartialUserFlow() should be deleted because it has no role in protocol validation. If the project requires FFI
elsewhere, its use should be limited to clearly justified, auditable scripts or tests that do not execute arbitrary host
commands.
function test_PartialUserFlow() public {
vm.prank(user1);
festivalPass.buyPass{value: VIP_PRICE}(2);
assertEq(beatToken.balanceOf(user1), 5e18);
vm.startPrank(organizer);
uint256 perf1 = festivalPass.createPerformance(block.timestamp + 1 hours, 2 hours, 50e18);
uint256 perf2 = festivalPass.createPerformance(block.timestamp + 4 hours, 2 hours, 75e18);
vm.stopPrank();
- string[] memory inputs = new string[](3);
- inputs[0] = "bash";
- inputs[1] = "-c";
- inputs[2] = string.concat(...);
- vm.ffi(inputs);
vm.warp(block.timestamp + 90 minutes);
vm.prank(user1);
festivalPass.attendPerformance(perf1);
assertEq(beatToken.balanceOf(user1), 5e18 + 100e18);
vm.warp(block.timestamp + 4.5 hours);
vm.prank(user1);
festivalPass.attendPerformance(perf2);
assertEq(beatToken.balanceOf(user1), 5e18 + 100e18 + 150e18);
}