使用 ShellCheck 进行静态分析与审计
将 ShellCheck 集成到安全门禁中,并解读其发现以加固每个脚本。
使用 ShellCheck 进行静态分析与审计 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。
ShellCheck 是什么以及它为何重要
ShellCheck 是一款开源的 shell 脚本静态分析工具。它会解析 Bash(以及 POSIX sh、dash、ksh)源代码而不执行代码,并报告错误、不安全的构造、可移植性问题和风格问题;每条报告都会带有唯一的规则代码,例如 SC2086。
在经过安全加固的流水线中,ShellCheck 充当必经检查关卡:脚本通过检查之前不得发布。这一点很重要,因为:
- 许多 shell 漏洞(单词拆分、注入、未加引号的展开)在正常路径测试期间并不明显,却会在攻击者控制的输入下触发。
- ShellCheck 可以在运行前、零成本地捕获这类错误。
- 它会说明每种模式为何危险,随着时间推移帮助团队提高安全意识。
请在任意系统上安装它:
# Debian / Ubuntu
sudo apt-get install shellcheck
# macOS (Homebrew)
brew install shellcheck
# From source via Cabal (any platform)
cabal update && cabal install ShellCheck
# Verify
shellcheck --version首次运行 ShellCheck
最简单的调用方式是 shellcheck <script>。ShellCheck 会读取 shebang 行来确定 shell 方言,然后将检查结果输出到标准输出。
每条检查结果都包括:
- 文件和行号——确切位置
- 严重程度——
error、warning、info或style - SC 代码——稳定的规则标识符,可供查询或禁用
- 面向用户的说明——告诉您哪里出了问题,通常还会说明如何修复
运行下面的脚本,观察 ShellCheck 将生成的输出:
#!/usr/bin/env bash
# demo_bad.sh — intentionally flawed for ShellCheck demonstration
FILE=$1
if [ $FILE == '' ]; then
echo "No file given"
fi
cat $FILE | grep 'error' | wc -l解读 ShellCheck 输出和 SC 代码
对于上一场景中的脚本,ShellCheck 将生成类似以下的检查结果:
SC2086(警告)——请使用双引号以防止通配符展开和单词拆分——针对[ $FILE == '' ]中的$FILE以及cat $FILE中的$FILE。SC2039/SC3010(信息)——[ ] 中的 == 是 Bash 特有语法;POSIX 中请使用 =。SC2002(风格)——无用的 cat。建议使用 cmd < file,而不是 cat file | cmd。
每个 SC 代码都对应 https://www.shellcheck.net/wiki/SCxxxx 上的一个 wiki 页面,其中包含原理说明和修正后的示例。
该脚本的修正版如下:
#!/usr/bin/env bash
# demo_fixed.sh — ShellCheck-clean
FILE="$1"
if [ -z "$FILE" ]; then
echo "No file given" >&2
exit 1
fi
grep -c 'error' "$FILE"严重程度级别及应采取的措施
ShellCheck 会根据严重程度对每条检查结果进行分类。在安全关卡中,您应按以下方式处理:
- error——几乎可以确定是错误或安全漏洞。阻止构建并立即修复。例如:缺少 shebang 的
SC2148,未加引号的$?对应的SC2070。 - warning——高风险模式,通常可能被利用。阻止构建;修复问题或明确说明禁用检查的理由。例如:变量未加引号对应的
SC2086。 - info——目前可能正确,但不稳健或不可移植。除非明确不属于当前作用域,否则应在同一个 PR 中修复。
- style——仅涉及外观或 POSIX 偏好。在纯 Bash 代码库中建议遵循,但不是强制要求。
使用 --severity=warning,即可仅在出现警告及更高严重程度的问题时以非零状态退出——这是标准的安全关卡阈值:
#!/usr/bin/env bash
# gate.sh — fail CI on errors and warnings only
shellcheck --severity=warning scripts/*.sh
echo "ShellCheck exit code: $?"将 ShellCheck 集成为 CI 安全关卡
安全关卡只有在强制执行且自动化时才有用。下面的模式会将 ShellCheck 封装在 CI 步骤中,并完成以下操作:
- 查找仓库中的每个
.sh文件。 - 使用
--severity=warning和机器可读的 JSON 输出运行 ShellCheck。 - 如果存在任何检查结果,就使流水线失败(
exit 1)。 - 打印摘要,让工程师无需离开 CI 日志即可处理检查结果。
将此文件放入您的代码仓库,并从 CI 流水线(GitHub Actions、Jenkins、GitLab CI 等)中调用它:
#!/usr/bin/env bash
# ci/shellcheck_gate.sh
set -euo pipefail
SCRIPTS=$(find . -name '*.sh' -not -path './.git/*')
FAILED=0
for script in $SCRIPTS; do
echo "==> Checking: $script"
if ! shellcheck --severity=warning --format=tty "$script"; then
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
echo "[GATE] ShellCheck found warnings or errors. Build blocked." >&2
exit 1
fi
echo "[GATE] All scripts passed ShellCheck."SC2086 系列:未加引号的变量展开
SC2086 是 ShellCheck 最常见的检查结果,也是最容易被利用的 shell 漏洞之一:未加引号的变量展开。
变量没有使用双引号括起时,shell 会对其值执行单词拆分(根据 IFS 拆分)和通配符展开。如果攻击者能够控制该变量,就可以注入额外参数、触发文件系统遍历,或使命令接收到意外的操作数。
经典的危险模式:
#!/usr/bin/env bash
# Attacker sets: FILENAME="important.txt /etc/passwd"
FILENAME="$1"
# UNSAFE — word splitting turns this into two args
rm $FILENAME
# SAFE — double quotes prevent splitting
rm "$FILENAME"
# Arrays are the right tool for lists
FILES=("$@")
rm -- "${FILES[@]}"使用 SC2046 和 SC2035 检测命令注入风险
两个不太为人熟知但十分关键的规则,专门处理通过子 shell 输出进行命令注入的问题:
SC2046——请在$(…)中使用引号,以防止单词拆分和通配符展开。如果子 shell 的输出未加引号,输出中的任何空白字符或通配符都会变成 shell 令牌。SC2035——请使用./*.sh而不是*.sh,以避免以-开头的文件名被解释为选项(这是经典的参数注入途径)。
具体的利用场景及修复方法:
#!/usr/bin/env bash
# SC2046 example — output of find fed unquoted to chmod
# If a filename contains spaces, extra arguments appear
# UNSAFE
chmod 600 $(find /secrets -name '*.key')
# SAFE — use a while-read loop or xargs with -0
find /secrets -name '*.key' -print0 \
| xargs -0 chmod 600
# SC2035 example
# UNSAFE — a file named '-rf' would be passed as an option
rm *.sh
# SAFE
rm -- ./*.sh使用 JSON 输出格式实现自动化
ShellCheck 通过 --format 支持多种输出格式:
tty(默认)——便于人阅读的终端输出json——机器可读;非常适合仪表板、自定义阻断规则或上传到 SAST 平台gcc——兼容解析 GCC 错误格式的工具(IDE、Vim/Emacs)checkstyle——供 Jenkins Checkstyle 插件使用的 XML 格式
JSON 格式让您能够编写自动化策略,例如仅针对特定的 SC 代码进行阻断,或将大型代码库中的检查结果汇总到安全报告中。
#!/usr/bin/env bash
# Emit JSON and filter for only error-severity findings using jq
shellcheck --format=json scripts/deploy.sh \
| jq '[.[] | select(.level == "error")]'
# Count distinct SC codes across all scripts
find . -name '*.sh' -print0 \
| xargs -0 shellcheck --format=json 2>/dev/null \
| jq '[.[] | .code] | group_by(.) | map({code: .[0], count: length}) | sort_by(-.count)'正确抑制误报
无差别地禁用 ShellCheck 会使它失去意义。正确的方法是进行有针对性且有文档说明的抑制,只影响检查结果确实不适用的特定行或代码块。
有三种抑制机制:
- 行内禁用——在有问题的代码上一行写入
# shellcheck disable=SC2086。只影响该行。 - 代码块禁用/启用——使用
# shellcheck disable=…和# shellcheck enable=…包围某个代码段。 - 文件级指令——在文件顶部放置
# shellcheck disable=…(很少有充分理由这样做;请记录原因)。
每条抑制都必须包含注释,解释为什么该检查结果属于误报:
#!/usr/bin/env bash
# deploy.sh
# Legitimate suppression: $DEPLOY_ARGS is intentionally word-split
# because it is a pre-validated list of flags from a trusted config file.
# shellcheck disable=SC2086
exec deploy-tool $DEPLOY_ARGS
# Block suppression for a section that generates dynamic code
# shellcheck disable=SC2016
VARS='$HOME $PATH $USER'
echo "Unexpanded vars: $VARS"
# shellcheck enable=SC2016通过 .shellcheckrc 配置 ShellCheck
对于项目范围的设置,ShellCheck 会从脚本所在目录开始向上查找 .shellcheckrc,一直查找到 /。这样可以避免每次调用都重复指定选项,并让关卡脚本保持简洁。
.shellcheckrc 中的实用指令:
shell=bash——覆盖方言检测(适用于没有 shebang 的文件)enable=all——启用可选检查(例如avoid-nullary-conditions、require-variable-braces)disable=SC2059——针对有充分理由的例外进行项目范围的抑制external-sources=true——跟踪并检查source/.指令
# .shellcheckrc — project root
shell=bash
enable=all
external-sources=true
# SC2312: consider invoking this command separately to avoid masking its
# return value — suppressed project-wide because we use set -e.
# Rationale: errexit already aborts on failure; masking risk is mitigated.
disable=SC2312端到端加固脚本:前后对比
真正掌握 ShellCheck 检查结果的最有效方法,是将一个真实的脚本从检查失败的状态重构为整洁且经过加固的版本。下面的脚本用于备份目录,编写时没有考虑安全性。它至少会因五条不同的规则而无法通过 ShellCheck。
请研究这两个版本。后一个版本在没有任何抑制指令的情况下通过了 shellcheck --severity=warning,并且在攻击者控制的输入下安全得多:
#!/usr/bin/env bash
# BEFORE — multiple ShellCheck violations
DEST=$1
SRC=$2
DATE=`date +%Y%m%d`
if [ ! -d $DEST ]; then
mkdir $DEST
fi
cp -r $SRC $DEST/$DATE
echo Done
#!/usr/bin/env bash
# AFTER — ShellCheck-clean and hardened
set -euo pipefail
DEST="${1:?Usage: backup.sh <dest> <src>}"
SRC="${2:?Usage: backup.sh <dest> <src>}"
DATE=$(date +%Y%m%d)
if [ ! -d "$DEST" ]; then
mkdir -p -- "$DEST"
fi
cp -r -- "$SRC" "$DEST/$DATE"
echo 'Done' >&2知识检查:安全关卡中的 ShellCheck
请检验您对 ShellCheck 作为安全关卡所承担角色的理解。
回顾:作为安全关卡的静态分析
在本课中,您学习了如何将 ShellCheck 设为 Bash 工作流中的强制安全关卡:
- ShellCheck 会在不执行脚本的情况下进行静态分析,在运行前捕获引号错误、注入风险和不安全模式。
- 每条检查结果都带有一个SC 代码(例如
SC2086),并链接到详细的文档和修复指导。 - 严重程度阶梯——
error、warning、info、style——让您能够调整关卡策略:--severity=warning是推荐的安全阈值。 - 使用机器可读的输出(
--format=json)来自动生成报告、跟踪趋势并集成 SAST。 - 谨慎使用抑制:始终只针对单行,始终在注释中说明原因;除非有
.shellcheckrc作为依据,否则绝不要全局抑制。 - 将 ShellCheck 与
set -euo pipefail、明确的引号、-- argument终止符以及输入验证结合起来,构建纵深防御。
通过 ShellCheck 的脚本并不会自动变得安全,但未通过 ShellCheck 的脚本绝不应进入生产环境。
常见问题解答
「使用 ShellCheck 进行静态分析与审计」课时是免费的吗?
是的 — 「使用 ShellCheck 进行静态分析与审计」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。
「使用 ShellCheck 进行静态分析与审计」这节课中我会学到什么?
将 ShellCheck 集成到安全门禁中,并解读其发现以加固每个脚本。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DevOps Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「使用 ShellCheck 进行静态分析与审计」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DevOps Bootcamp 课中编写并运行代码吗?
能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 防止命令与参数注入
- 安全处理机密信息与保持环境整洁
- 最小权限执行与 sudo 规范
- 使用 ShellCheck 进行静态分析与审计