编写 shell 脚本给犯错留出了许多空间，以导致你的脚本会被特定的输入终止或者在碰到不受信任的输入时触发一些公开的安全漏洞。以下是一些让你编写更加安全的 shell 脚本的一些技巧。
最简单的方法就是不使用 shell 脚本。许多高级语言都更容易编写代码，并且也可以避免一些 shell 中有的问题。例如，当你用试图读取一个未初始化的变量（虽然往里面写入并不会）或者是一些函数调用出错的时候，Python 都会自动返回报错信息并结束程序。
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 subprocess module is very useful for this. You should try to avoid passing
subprocess (or using
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 word-splitting 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 append
|| 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 https://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:
alex@kronborg tmp [15:23] $ dir="foo bar" alex@kronborg tmp [15:23] $ ls $dir ls: cannot access foo: No such file or directory ls: cannot access bar: No such file or directory alex@kronborg tmp [15:23] $ cd "$dir" alex@kronborg foo bar [15:25] $ file=*.txt alex@kronborg foo bar [15:26] $ echo $file bar.txt foo.txt alex@kronborg 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:
See “Special Parameters” in the bash manual for details on the distinction between
"$@" — the first and second are rarely what you want in a safe shell script.
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
remctl, perhaps), consisting of just
sudo -u nobody "$@". The quoting is fine, but if a user passes
-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 -- "$@" would avoid this attack (though obviously specifying in the
sudo configuration that commands can only be run as
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.
Use ShellCheck to check for bugs
The ShellCheck linter automatically catches a number of the above mistakes and more. Run it regularly, ideally with integration into your editor and your test suite, and address all of its diagnostics. Even warnings that might sound unimportant could be obscuring important bugs.
Google 有一份 Shell 风格指南。很明显，这份指南主要关注于 Google 代码风格，但是也有一些部分跟安全有关。
如果可能的话，尽量用像 Python 这样的高级语言去写而不是去写 “安全” 的 shell 脚本。如果非要用 shell 的话，有一些 shell 的选项配置可以降低出 bug 的风险并且记得使用引号。