深色模式
脚本安全规范
摘要:脚本往往以高权限运行、批量影响多台机器,一旦存在漏洞就是放大器。本文给出运维脚本的六条安全规范:防命令注入、变量加引号、临时文件安全、凭证不落盘、最小权限、输入校验,并附一份上线前检查清单。
适用环境
bash
which shellcheck sudo mktemp
python3 --version
sudo -V | head -11
2
3
2
3
操作步骤
一、防命令注入:绝不拼接用户输入
bash
# 危险:用户输入直接拼进命令
read -r name
eval "echo hello $name" # 输入 "; rm -rf /" 就会执行删除
# 危险:反引号或 $() 里带变量
rm -rf $DIR/* # DIR 为空时变为 rm -rf /*1
2
3
4
5
6
2
3
4
5
6
python
# 危险:shell=True + 字符串拼接
import subprocess
subprocess.run(f"ls {user_input}", shell=True) # 注入入口
# 安全:列表形式传参,不经过 shell
subprocess.run(["ls", user_input], timeout=30)1
2
3
4
5
6
2
3
4
5
6
危险
eval、shell=True、反引号执行命令,只要其中含有外部输入(用户参数、配置文件、接口返回、文件名),就构成命令注入。运维脚本里这两者应当默认禁用,只有完全无外部输入的固定命令才可例外。
二、变量一律加引号
bash
# 危险
cp $src $dst
rm -rf $path/
# 安全
cp "$src" "$dst"
rm -rf -- "${path:?path 不能为空}/"1
2
3
4
5
6
7
2
3
4
5
6
7
${var:?} 在变量为空时直接报错退出,是防止 rm -rf / 类事故的有效手段。-- 用于表示选项结束,避免以 - 开头的文件名被当成参数。
三、临时文件必须用 mktemp
bash
# 危险:固定路径可被预测,存在符号链接攻击与竞态
echo data > /tmp/ops.tmp
# 安全:随机文件名 + 严格权限
tmp="$(mktemp /tmp/ops.XXXXXX)"
chmod 600 "$tmp"
trap 'rm -f "$tmp"' EXIT1
2
3
4
5
6
7
2
3
4
5
6
7
python
import tempfile, os
fd, path = tempfile.mkstemp(prefix="ops-", suffix=".tmp")
os.close(fd)
os.chmod(path, 0o600)1
2
3
4
2
3
4
四、凭证不落盘、不入代码
bash
# 危险
mysql -u root -p'P@ssw0rd' ... # 密码进入 ps 输出与 shell history
# 安全做法一:配置文件设 600 权限
cat > ~/.my.cnf <<'EOF'
[client]
user=root
password=从密钥管理服务获取
EOF
chmod 600 ~/.my.cnf
# 安全做法二:环境变量注入,用完清除
export MYSQL_PWD="$(get_secret_from_vault)"
mysql -u root ...
unset MYSQL_PWD
# 清理历史记录中的敏感命令
history -d $((HISTCMD-1)) 2>/dev/null || true1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python
import os
# 缺失即失败,不用默认值硬跑
token = os.environ.get("API_TOKEN")
if not token:
raise SystemExit("缺少 API_TOKEN 环境变量")1
2
3
4
5
2
3
4
5
注意
脚本一旦把凭证写进代码并提交 Git,即使后续删除,历史记录里依然可查。正确处置是立即轮换该凭证,而不是删除文件。
五、最小权限:不要万事 sudo
bash
# 危险:整脚本 root 运行
sudo ./deploy.sh
# 更好:只在需要的步骤提权,并限定可执行的命令
# /etc/sudoers.d/deployer
# deployer ALL=(root) NOPASSWD: /bin/systemctl restart myapp, /usr/bin/nginx -t
sudo -l -U deployer # 查看该用户可提权的命令1
2
3
4
5
6
7
2
3
4
5
6
7
脚本自身权限也要收敛:
bash
chmod 750 /usr/local/bin/ops-script.sh
chown root:ops /usr/local/bin/ops-script.sh
# 脚本目录不可被普通用户写入,否则等于给了提权入口
chmod 755 /usr/local/bin1
2
3
4
2
3
4
六、输入校验与白名单
bash
# 危险:直接把参数当环境名用
env="$1"
cat "/etc/app/${env}.conf"
# 安全:白名单校验
case "$1" in
prod|staging|test) env="$1" ;;
*) echo "非法环境: $1" >&2; exit 2 ;;
esac1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
python
ALLOWED = {"prod", "staging", "test"}
if env not in ALLOWED:
raise SystemExit(f"非法环境: {env}")1
2
3
2
3
七、下载执行类操作要校验
bash
# 危险:直接管道执行远程脚本
curl -sSL https://example.com/install.sh | bash
# 更安全:先下载 → 校验 → 人工确认 → 执行
curl -sSL -o /tmp/install.sh https://example.com/install.sh
sha256sum /tmp/install.sh # 与官方公布的校验值比对
less /tmp/install.sh # 至少扫一眼
bash /tmp/install.sh1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
八、上线前检查清单
bash
# 1) 静态检查
shellcheck --severity=error script.sh
# 2) 语法检查(不执行)
bash -n script.sh
python3 -m py_compile script.py
# 3) 检查是否含硬编码凭证
grep -nE "password|secret|token|AKIA|BEGIN .*PRIVATE KEY" script.sh
# 4) 检查危险写法
grep -nE '\beval\b|shell=True|rm -rf \$|chmod 777|: \(\)' script.sh script.py
# 5) 权限检查
stat -c '%a %U:%G %n' script.sh1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
验证
- [ ] 脚本中不存在
eval/shell=True与外部输入拼接 - [ ] 所有变量引用都带引号
- [ ] 临时文件使用
mktemp,权限为600,且有清理钩子 - [ ] 凭证从环境变量或密钥管理服务获取,仓库中无明文
- [ ] 脚本文件权限非
777,所在目录不可被非授权用户写入 - [ ] 参数经过白名单校验
bash
shellcheck --severity=error script.sh && bash -n script.sh && echo "安全检查通过"1
常见坑
rm -rf $VAR/*:变量为空时变成rm -rf /*,这是最经典的运维事故之一。- 脚本目录可写:普通用户能替换脚本内容,而脚本以 root 定时运行,等于直接提权。
- 把密码写进
ps可见的命令行:同机器的任何用户都能看到。 chmod 777图省事:任何人都能改脚本,且往往被定时任务高频执行。- 忽略下载的第三方脚本校验:供应链攻击最常见的入口就是「复制粘贴一行安装命令」。