The API used handles string interpolation correctly: the string literal is parsed at compile time, and the interpolated arguments are never concatenated or escaped, and end up directly as an element of arv array passed to a child. See
https://github.com/tigerbeetle/tigerbeetle/blob/7053ecd2137a...
comptime assert(std.mem.indexOfScalar(u8, cmd, '\'') == null); // Quoting isn't supported yet.
comptime assert(std.mem.indexOfScalar(u8, cmd, '"') == null);
But you can do correct interpolation with simple shell variables, rather than generating shell code strings: $ today='foo; bar' sh -c 'argv git switch --create "release-$today" origin/main'
['git', 'switch', '--create', 'release-foo; bar', 'origin/main']
So that is a test that we can use a plain shell string, without any shell injection bug. (argv is my command to test quoting: python -c 'import sys; print(sys.argv)' "$@" )Note that there's no escaping function needed, because we're not generating any shell code. We're generating an argv array for `/bin/sh` instead.
---
So by invoking with an env var, you can easily create a correct API that uses plain shell
git switch --create "release-$today"
rather than git switch --create release-{today} # what language is this? It's not obvious
If you don't want to use the env var, you can also use git switch --create "release-$1"
And invoke with ['sh', '-c', shell_string, 'unused-arg0', today_string]
With this approach, you don't need 1. any kind of shell escaping
2. any analyzing of pseudo-shell strings, which can't contain quotes
Because you are not generating any shell code. The shell code is constant. (sh "cat " file " >" output)
With file being bound to "foo'bar" and output to "x", it is automatically translated into cat 'foo'\''bar' >'x'
This gives you the flexibility to use shell (sometimes it just is the most concise way) while being safe against injection.I believe for example in rust you should be able to do the same.
If you’re averse to this:
q(“select x where y = ‘“ + v + “‘“)
And instead do this: q(“select x where y = %s”, v)
Then you should be averse to this: x(“foo --option ‘“ + v + “‘“)
And instead do this: x(‘foo --option “$1”’, v)
This is particularly useful when it’s expedient to have one thing piping into another. Like it or not the sh DSL for pipes is excellent compared to doing things natively with execve() and pipe(), just as doing group by and count is far more concise in SQL than doing so natively.Most SQL libraries give you something like q. Writing your own x is as simple as calling sh correctly. In Python, for example:
def x(script, *args):
run([“sh”, “-c”, script, “--“, *args]) SyntaxError: invalid character '“' (U+201C)Your python example at the bottom is correct, in that each separate element is more correct in that it allows each arg to be passed as an element, so there's no option to break out through quoting characters. SQL binds are like that in most ljbraries, even if they don't look like it. The parser knows a single item below there so if it passes it along as such. You cannot escape it in the same way.
sh is smarter than just doing string interpolation and ”$1” is passed on as a single argument, no matter what:
> run(["sh", "-c", 'echo "$1"', "--", 'a"'])
a”
Whereas if it were simple string interpolation, you’d see this: > run(["sh", "-c", 'echo "a""')
--: 1: Syntax error: Unterminated quoted string
It’s the same special casing that gets "$@" right. # cat pvars.sh
#!/bin/bash
echo "\$1=$1"
echo "\$2=$2"
echo "\$3=$3"
# sh -c './pvars.sh "$1" $2 $3' -- 'a b' 'c d'
$1=a b
$2=c
$3=d
The whole point of passing in an array and using something like exec (or system(), if provided as it handled the fork and wait for you) is that you avoid the overhead of the shell starting up at all and parsing the command line, and it lets you define each param exactly as needed since each param is its own array item. You don't need to worry about splitting on space or the shell splitting params on space, or quoting to group items. If you want the param to be: foo "bar baz" quux
as one singular parameter, you just make that the contents of that array item, since no parsing need be done at all.If you have an array of params and you're jumping through hoops to make sure they're interpreted correctly by the shell you call execute a process, you're likely (depending on language and capabilities) wasting both cycles and over complicating the code when you can just call the program you actually want to execute directly and supply the params. Alternatively, if you have all the params as one long string and you want it to be pased as a shell would, then execute the shell and pass that as a param. e.g.
# perl -E 'system("./pvars.sh","a b","c d");'
$1=a b
$2=c d
$3=
# perl -E 'system("./pvars.sh","a b","c","d");'
$1=a b
$2=c
$3=dBut for a simple example like this where it's inserting a date which has known properties, it seems fine, and is much more readable.
But at the very least the shell is unnecessary here.
run("program --option '{user_input}' > file")
to save some input to a file, and the user's input is: '; bad_command #
then when run() sends that string to the shell, the shell will run: program --option '';
bad_command #' > file
Most languages have something like a safe_exec() that separates the shape of the command from the values of the options, executing "program" with the options and the user_input in the arguments array as data. Skipping the shell step, which would just be building an exec call anyway, removes the opportunity for users to confuse it into doing something else.The list-based API alternative they recommend might look like this:
safe_exec(["program", "--option", user_input], stdout="file")
and it would always exec "program" with argv[1] == "--option" && argv[2] == user_input. If the user_input happens to be: '; bad_command #
...well, then, the user can enjoy the contents of their file. safe_exec(["rm", user_input])
This isn't safe either! Despite clearly saying "safe_exec"!Shelling out is not the only option. People are just saying not to use that option. Better ones won't save you if you purposely do something stupid. They will save you if the user wants to trick you into doing something else.