; Echo “Shell Injection”
matklad.github.io
matklad.github.io
https://bonedaddy.net/pabs3/log/2014/02/17/pid-preservation-...
Another issue to worry about when executing other processes is option injection:
https://www.defensecode.com/public/DefenseCode_Unix_WildCard...
Its to ensure you can take a shell oneliner and turn it into a program one liner. Ie. anything you can do in a terminal / copy from the web you can trivially do in our program! Whilst this is a footgun for application development this is a necessity for other kinds of people who write programs that are also a target market for such scripting languages.
Of course it should really be called 'runInShell', and provide an option for which shell etc.
Every language the author lists as vulnerable was intended, at launch, to be used both as a serious programming language as well as program launching glue or by non-career programmers for things that aren't production grade applications.
The only language I can think of that is explicitly an application development language is Rust, and as the author mentions this vuln is not present.
And as a note, the alternative to exec in NodeJS are keeping execFile and using these ~10 lines [1] or using spawn directly with these 216 lines [2]. I barely trust myself to reliably reproduce those.
[1] https://github.com/nodejs/node/blob/df25424b9195d31224529895...
[2] https://github.com/nodejs/node/blob/df25424b9195d31224529895...
> Of course it should really be called 'runInShell'
const { stdout, stderr } = await exec(`curl ${line}`);
Javascript has fetch(), PHP has a curl library built into the language, python has requests, etc. The majority of shell() calls have the same exact situation.
Why does it not work so:
exec(['curl', line])
Like in about any other language?I don't even see a need or something like that to exist, if one truly must, then one can always use:
exec(['/bin/sh', '-c', 'curl ${line}'])
Which I assume it resolves under the hood. exec('curl ${line}')
Which takes a single string as argument, whereas mine is: exec(['curl', line])
Which takes an array of strings as an argument, which are not parsed by any shell interpreter.Of course, flags can still be passed to curl that way, so the proper way is:
exec(['curl', '--', line])The exec() documentation actually provides a warning:
> Never pass unsanitized user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution.
Likewise, spawn() says this:
> If the shell option is enabled, do not pass unsanitized user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution.
I do think the naming could be better - why isn’t exec() called shell()?
[0]: https://nodejs.org/api/child_process.html#child_process_chil...
[1]: https://nodejs.org/api/child_process.html#child_process_chil...
If it really be needed, one can always do something such as:
exec(['/bin/sh', '-c', "curl ${line}"])That said, I often use such code myself for simple tasks that I run on a local machine. The simplicity of calling a shell, internal pipes and all of its options, is great. If and when I need to polish it or especially expose this to other users I can rewrite it properly. My 2c.
I wrote a little bit about this related issue here: https://jmmv.dev/2020/11/cmdline-args-unix-vs-windows.html
I would warn that "CommandLineToArgvW" does not necessarily do what the C runtime does. They are different implementations.
Also your main problem in that post seems to have been in using `cmd.exe` and DOS utilities. The command prompt is all about mimicking DOS and not breaking .bat files from 80s and 90s. It's essentially stuck in stasis. Powershell is the actively developed shell (now on version 7).
PowerShell can do things better as long as you stay within its domain, but things break down when you talk to other binaries. Touched upon that here https://jmmv.dev/2020/10/powershell-cmdlet-params.html ;)
On Unix, it is not very intuitive because you cannot use backslash inside a single quoted string. To quote "o'neil" we have to write
'o'\''neil' 'o'\''neil'
You can always do the weird one, which I find easier to remember (but is likely so for no one else), of dropping to double quotes for the single quote: 'o'"'"'neilIt is a bit sad to read that.
Spawning applications from your application is dangerous, so you need to be careful, and at least read the documentation.
This shows that a lot of developers nowadays just copy-paste code from StackOverflow, don't read the documentation and don't know the real low-level calls in a soup of unorganised abstractions and libraries.
This behaviour is documented, is actually intended, and is all explained:
https://nodejs.org/api/child_process.html#child_process_chil...
There is even a warning: "Never pass unsanitized user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution." few lines later: "do not pass unsanitized user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution."
Please read the manual before you want to operate a dangerous machine.
In this particular case they know you might need to pass a bunch of completely unrelated strings, and they know their underlying primitive can't really do that, and so they... provide code that just concatenates all your strings, as if you're a programmer who doesn't know how to concatenate strings and needs help with that.
The best excuse I can imagine is somebody at VSCode assigned a junior the task of writing the trivial "escape everything properly and concatenate it" function. Except, that function isn't trivial, and the junior lacked confidence to come back with the answer "This isn't trivial after all, I need a grown-up" so they hacked up a solution that doesn't solve anything, checked it in and breathed a sigh of relief when it passed review.
Separately from the factual response, I want to explicitly say that I don’t endorse such style of communication. Please do not blindly assume, and then pick on, on the lack of knowledge of other people (be it author of the post or abstract lot of developers).
Even when you actually find the lack of knowledge (rather than assuming it), do not scold people for it. It’s OK to not know things, we all learned them for the first time once.
My comment was more about the situation where a newbie knows that he is handling dangerous operations but still copy-paste from StackOverflow without even thinking or reading.
You went one-step ahead and wrote a summary so it wasn't about your situation.
The only point where I fundamentally disagree is about ShellExecution.
When you send a string to the shell for execution, it seems reasonable to expect >, semi-colons, pipes, and other shell operators to be interpreted by the shell unless you escape them.
Fully agree here. But the problem is the second overload, which can take an array of strings (so looks like a safe API), but concatenates them without escaping.
In the ShellExecution function, if you actually comes from C programming you can get confused:
Could the parameter "string | ShellQuotedString" mean:
"A string which has the binary flag ShellQuotedString on" = "A shell quoted/escaped string" +/- "a string of sub-type ShellQuotedString" which would imply that the developer has to take this responsibility.
or it means "string || ShellQuotedString", as in param is a "string OR ShellQuotedString" which in this case the behaviour is unknown.
(well, now I know it's the second one :| ) but it's fun to read
From the article:
> I would have written this in Rust, but, alas, it’s not vulnerable to this particular attack :)
But of course, strictly speaking, this isn't the case, as you can always call sh -c and rust doest prevent you from doing that (though makes it harder).
Whereas standard libraries that silently fork sh are less safer because they introduce the shell injection vulnerability by default thus requiring developers to consciously be aware of the need to sanitise their inputs.
As I've mentioned elsewhere, I personally think it was a boneheaded decision to ever make exec() functions call a shell. Maybe I'll forgive Perl because that has it's origins as being an extension to shell scripting. But by the time node.js was released people really should have known better.
While that's true, some language's standard libraries don't expose this vulnerability so the developer has to explicitly write bad code. And that's what really matters.
Take node.js for example: it's not going to be obvious that `exec(command)` is actually equivalent to `sh -c "$command"` and thus a lot of people will get caught short if they have unescaped shell script tokens (eg $, <, >, ;, etc) in their command string. And that's very easy to happen.
Personally I think it was a boneheaded decision when most of the languages adopted that approach of forking to sh. If developers wanted to fork to a shell then they could still explicitly write the code to do so (like the `spawn("/bin/sh", "-c", cmd)` example in the article). But by having exec fork sh by default you're creating a risk for all sorts of unexpected behaviour from developers who don't know their language's standard library inside out (which, lets be honest, is going to be most developers given the point of standard libraries is to abstract away that complexity so developers don't need to think about it).
This isn't just a web scale problem either. In fact the example given in that article is a perfect one because its a CI/CD pipeline running trusted input that still failed unexpectedly.
Specifically for PHP SQL injection, you can use PDO's prepared statements, and for shell commands you have https://www.php.net/manual/en/function.escapeshellcmd.php and https://www.php.net/manual/en/function.escapeshellarg.php
subprocess.run_escaped("tac -- %s | grep 1 > %s", "foo.txt", "bar.txt")
over this: with open("bar.txt", "w") as outfile:
tac = subprocess.Popen(["tac", "--", "foo.txt"], stdout=subprocess.PIPE)
grep = subprocess.Popen(["grep", "1"], stdin=tac.stdout, stdout=outfile)
tac.wait()
grep.wait()
(In practice the shell grammar is so grotesque that I'd never trust it to be 100% correct. But the idea is nice.)Plus if you’re going to those lengths then you’re better off embedding a Lua/Python/Perl/whatever interpreter in your application.