This article comes from Writing Safe Shell.
Writing shell scripts leaves a lot of room to make mistakes, in ways that will cause your scripts to break on certain input, or (if some input is untrusted) open up security vulnerabilities. Here are some tips on how to make your shell scripts safer.
The simplest step is to avoid using shell at all. Many higher-level languages are both easier to write the code in in the first place, and avoid some of the issues that shell has. For example, Python will automatically error out if you try to read from an uninitialized variable (though not if you try to write to one), or if some function call you make produces an error.
One of shell's chief advantages is that it's easy to call out to the huge variety of
command-line utilities available. Much of that functionality will be available through
libraries in Python or other languages. For the handful of things that aren't, you can
still call external programs. In Python, the
module is very useful for this. You should try to avoid passing
os.system or similar functions at all), since that will run a shell, exposing
you to many of the same issues as plain shell has. It also has two big advantages over
shell — it's a lot easier to avoid
or similar issues, and since calls to
subprocess will tend to be relatively uncommon,
it's easy to scrutinize them especially hard. When using
subprocess or similar tools,
you should still be aware of the suggestions in "Passing filenames or other positional
arguments to commands" below.
POSIX sh and especially bash have a number of settings that can help write safe shell scripts.
I recommend the following in bash scripts:
set -euf -o pipefail
set -o doesn't exist, so use only
What do those do?
If a command fails,
set -e will make the whole script exit, instead of just resuming
on the next line. If you have commands that can fail without it being an issue, you can
|| true or
|| : to suppress this behavior — for example
set -e followed by
false || : will not cause your script to terminate.
Treat unset variables as an error, and immediately exit.
Disable filename expansion (globbing) upon seeing
If your script depends on globbing, you obviously shouldn't set this. Instead, you may find
shopt -s failglob
useful, which causes globs that don't get expanded to cause errors, rather than getting
passed to the command with the
set -o pipefail causes a pipeline (for example,
curl -s http://sipb.mit.edu/ | grep foo) to produce a failure return code if any command errors. Normally, pipelines only return a failure if the last command errors. In combination with
set -e, this will make your script exit if any command in a pipeline errors.
For example, consider the following:
[email protected] tmp [15:23] $ dir="foo bar" [email protected] tmp [15:23] $ ls $dir ls: cannot access foo: No such file or directory ls: cannot access bar: No such file or directory [email protected] tmp [15:23] $ cd "$dir" [email protected] foo bar [15:25] $ file=*.txt [email protected] foo bar [15:26] $ echo $file bar.txt foo.txt [email protected] foo bar [15:26] $ echo "$file" *.txt
Depending on what you are doing in your script, it is likely that the word-splitting and
globbing shown above are not what you expected to have happen. By using
"$foo" to access
the contents of the
foo variable instead of just
$foo, this problem does not arise.
When writing a wrapper script, you may wish pass along all the arguments your script received. Do that with:
wrapped-command "[email protected]"
"Special Parameters" in the bash manual
for details on the distinction between
[email protected], and
"[email protected]" — the first and second are
rarely what you want in a safe shell script.
Passing filenames or other positional arguments to commands
If you get filenames from the user or from shell globbing, or any other kind of
positional arguments, you should be aware that those could start with a
"-". Even if you
quote correctly, this may still act differently from what you intended. For example,
consider a script that allows somebody to run commands as
nobody (exposed over
perhaps), consisting of just
sudo -u nobody "[email protected]". The quoting is fine, but if a user
-u root reboot,
sudo will catch the second
-u and run it as
Fixing this depends on what command you're running.
For many commands, however,
-- is accepted to indicate that any options are done,
and future arguments should be parsed as positional parameters — even if they look like
options. In the
sudo example above,
sudo -u nobody -- "[email protected]" would avoid this attack
(though obviously specifying in the
sudo configuration that commands can only be run
nobody is also a good idea).
Another approach is to prefix each filename with
./, if the filenames are expected to be in the current directory.
A common convention to create temporary file names is to use
something.$$. This is not
safe. It is better to use
Google has a Shell Style Guide. As the name suggests, it primarily focuses on good style, but some items are safety/security-relevant.
When possible, instead of writing a "safe" shell script, use a higher-level language like Python. If you can't do that, the shell has several options that you can enable that will reduce your chances of having bugs, and you should be sure to quote liberally.