Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Malicious vm.ffi command in the test suite enables arbitrary code execution on auditor and CI hosts

Root + Impact

Root Cause: foundry.toml enables FFI globally (ffi = true) and test/FestivalPass.t.sol passes an attacker-authored bash -c command to vm.ffi, so running the test suite executes arbitrary shell on the host machine instead of inside the EVM.

Impact: Any auditor or CI runner that executes forge test runs the embedded shell command with their own environment, secrets, and filesystem access. A one-line change to the payload achieves full remote code execution and credential exfiltration on every developer and CI machine.

Description

  • Normal behavior: a Foundry test exercises on-chain logic inside the EVM and must not run host commands; FFI should be disabled, or scoped to a dedicated profile that CI never runs.

  • The bug: test_PartialUserFlow builds inputs = ["bash", "-c", <script>] and passes it to vm.ffi(inputs). With ffi = true in the default profile, that command runs on the machine of whoever runs the suite. The shipped payload only prints fake "wallet extraction" theater to /dev/tty, but the primitive is unrestricted: swapping the command exfiltrates ~/.ssh, environment variables, or wallet keys. The CI workflow runs the full suite on every push and pull request.

// test/FestivalPass.t.sol (inside test_PartialUserFlow)
string[] memory inputs = new string[](3);
inputs[0] = "bash";
inputs[1] = "-c";
inputs[2] = string.concat(
"echo -e '...[!] EXTRACTING WALLET DATA...' > /dev/tty; ", //@> attacker-controlled shell payload
"echo -e '...[*] Private Key: 0x...' > /dev/tty; ..."
);
vm.ffi(inputs); //@> arbitrary command execution on the host running `forge test`

Enabling configuration and automatic trigger:

foundry.toml:9 -> ffi = true (default profile, all tests)
.github/workflows/test.yml:39 -> forge test -vvv (runs on push / pull_request / workflow_dispatch)
setup.sh:12 -> --no-match-test test_PartialUserFlow (local-only mitigation; CI does NOT exclude it)

Risk

Likelihood:

  • Triggers every time an auditor runs forge test (the documented setup command) or CI runs the suite on push, pull request, or manual dispatch.

  • setup.sh excludes this one test locally, but the CI workflow runs the full suite with no exclusion, so the FFI fires automatically in CI.

Impact:

  • Arbitrary code execution on developer and CI hosts: theft of SSH keys, environment secrets, signing keys, and CI tokens.

  • Supply-chain compromise: a malicious pull request that edits the payload reaches every machine that runs the suite.

Proof of Concept

This is a tooling and supply-chain finding, not on-chain state, so there is no Foundry assertion to run. To observe the primitive firing, run the suite as shipped:

// 1. foundry.toml enables FFI for every profile:
ffi = true
// 2. test/FestivalPass.t.sol builds and runs a host command:
string[] memory inputs = new string[](3);
inputs[0] = "bash";
inputs[1] = "-c";
inputs[2] = "<any shell command the test author chooses>";
vm.ffi(inputs); // executed on the host, not in the EVM
// 3. Reproduce: `forge test --match-test test_PartialUserFlow -vvv`
// The bash payload runs and prints the fake extraction theater to the terminal.

Honest caveat: the shipped payload is inert theater that only writes to /dev/tty. The severity rests on the enabled arbitrary-RCE primitive plus automatic CI execution, not on demonstrated exfiltration. Weaponization is a one-line change to inputs[2] (for example reading ~/.ssh/id_rsa and curling it to an external host); it is described in prose deliberately and not executed.

Recommended Mitigation

Disable global FFI. If a test genuinely needs external data, read a file with vm.readFile and vm.parseJson rather than spawning a process, and never run an FFI-enabled profile in CI.

# foundry.toml
- ffi = true
+ # FFI disabled globally; tests must not spawn host processes.
+ # If external data is unavoidable, gate it behind a dedicated [profile.ffi]
+ # that CI never invokes, and use vm.readFile / vm.parseJson instead of vm.ffi.

Additionally, delete the vm.ffi block in test_PartialUserFlow.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 16 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!