Skip to content

Sourcing generated shell is still execution

 2 min Using /dev/stdin avoids eval’s call-site quoting hazard and keeps generated code inspectable; it does not reduce the injection surface.
Published

Mar 10, 2012

When a program generates shell statements for the current shell to execute, the trust decision has already been made: the output is code. Replacing eval does not make hostile output safe. It can still remove an avoidable layer of call-site quoting and make the exact program easier to inspect.

The alternative, from a Stack Overflow answer of mine, routes the generated text through the normal source path instead:

. /dev/stdin <<EOF
$(generate-shell-statements)
EOF

The mechanics: . (source) reads a file and executes it in the current shell. /dev/stdin makes that file be standard input, and the heredoc attaches the generated text as standard input. The herestring form is equivalent and more compact:

. /dev/stdin <<<"$(generate-shell-statements)"

What this buys over eval is structural, not cosmetic. eval joins its arguments into one command string and parses that string. With eval "$(generate)", the generated text reaches it as one argument and gets that one shell parse. Write eval $(generate) without the double quotes and the calling shell first word-splits and glob-expands the program text; eval then joins those altered arguments and parses them. The source path has no intermediate argument list. It parses the generated program the way it parses any script: a bounded input stream, read once, top to bottom. The program is visible as a normal stream—you can splice a tee or a pager onto the same construction to inspect exactly what is about to run, and what you inspect is what executes.

There is a second, less obvious win. In bash, . accepts arguments after the filename, which become the sourced code’s positional parameters. The generated program can therefore be generic—written once in terms of $1 and $2—with the varying values passed alongside rather than interpolated into the generated text. Every value kept out of the code is a quoting problem that no longer exists.

The idiom made it out of the answer and into production: shutils.jq compiles jira-nice’s trusted object and array shapes into typed declare statements so that one . /dev/stdin <<<"$(...)" line can land them in the shell. jq’s @sh protects values; identifier-safe keys are the separate boundary that keeps generated names from becoming active syntax. That call site still sources partial output if generation fails. Quoting preserves the program that arrived; the caller must separately check that generation succeeded before executing it.