🤔 Background
bashunit::runner::execute_test_body builds the per-test EXIT trap by interpolating the test file's path into the trap string:
trap"exit_code=\$?; bashunit::runner::cleanup_on_exit \"$test_file\"\"\$exit_code\"" EXIT
A trap body is re-evaluated by the shell when it fires, so a path containing a command substitution is executed at that point. The surrounding double quotes prevent word splitting but not substitution.
Confirmed on main: a test file named with an embedded `…` runs the command when the suite runs, and the $( … ) form behaves the same. It reproduces under --parallel too, where the trap is set inside a worker.
A filename is data. The exposure is any run over a tree whose filenames someone else controls — which is what a CI job does with a checked-out branch, so whoever can add a file to the tree chooses the command.
There is a second, quieter effect: the substitution consumes part of the name, so the path that gets sourced is not the file on disk. Such a test's assertions never run and it is reported risky rather than failing — a real failure disappears.
💡 Fix
Pass the path through a variable and single-quote the trap body, so nothing expands until the trap fires and then only as an ordinary parameter expansion, which does not re-scan its value.
src/coverage/engine.sh also installs traps from variables, but those hold bashunit's own code rather than external data; this was the only site interpolating a path.
💡 Note
Worth considering a GitHub Security Advisory and a prompt release rather than only a changelog entry — that call is yours.
🤔 Background
bashunit::runner::execute_test_bodybuilds the per-test EXIT trap by interpolating the test file's path into the trap string:A trap body is re-evaluated by the shell when it fires, so a path containing a command substitution is executed at that point. The surrounding double quotes prevent word splitting but not substitution.
Confirmed on
main: a test file named with an embedded`…`runs the command when the suite runs, and the$( … )form behaves the same. It reproduces under--paralleltoo, where the trap is set inside a worker.A filename is data. The exposure is any run over a tree whose filenames someone else controls — which is what a CI job does with a checked-out branch, so whoever can add a file to the tree chooses the command.
There is a second, quieter effect: the substitution consumes part of the name, so the path that gets sourced is not the file on disk. Such a test's assertions never run and it is reported
riskyrather than failing — a real failure disappears.💡 Fix
Pass the path through a variable and single-quote the trap body, so nothing expands until the trap fires and then only as an ordinary parameter expansion, which does not re-scan its value.
src/coverage/engine.shalso installs traps from variables, but those hold bashunit's own code rather than external data; this was the only site interpolating a path.💡 Note
Worth considering a GitHub Security Advisory and a prompt release rather than only a changelog entry — that call is yours.