5.7.2 变量与输入安全校验
变量与输入是脚本最主要的攻击面之一,本节聚焦“先校验、后执行”的安全原则,并配套可执行示例、安装与排错方法。
原理草图(输入进入脚本的安全关口):
安全基线与默认值处理示例(含命令解释):
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# 解释:
# -e 出错立即退出;-u 未定义变量视为错误;-o pipefail 管道任一失败返回失败
# IFS 防止空格分割引发意外
readonly APP_HOME="/opt/app" # 关键路径只读
PORT="${PORT:-8080}" # 默认值避免空值
LOG_DIR="${LOG_DIR:-/var/log/app}"
echo "APP_HOME=$APP_HOME"
echo "PORT=$PORT"
echo "LOG_DIR=$LOG_DIR"
输入校验与白名单函数库(可直接复用):
#!/usr/bin/env bash
set -euo pipefail
require_nonempty() {
local name="$1" value="$2"
if [[ -z "$value" ]]; then
echo "ERROR: $name is required" >&2
exit 1
fi
}
require_integer() {
local name="$1" value="$2"
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
echo "ERROR: $name must be integer, got '$value'" >&2
exit 2
fi
}
require_ip() {
local name="$1" value="$2"
if [[ ! "$value" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
echo "ERROR: $name must be IPv4, got '$value'" >&2
exit 3
fi
# 范围检查
IFS='.' read -r o1 o2 o3 o4 <<<"$value"
for o in $o1 $o2 $o3 $o4; do
((o>=0 && o<=255)) || { echo "ERROR: $name octet out of range" >&2; exit 4; }
done
}
require_safe_path() {
local name="$1" value="$2"
if [[ "$value" == *".."* || "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
echo "ERROR: $name contains unsafe path segment" >&2
exit 5
fi
}
# 示例调用
USER_NAME="${1:-}"
PORT="${2:-}"
TARGET_IP="${3:-}"
TARGET_PATH="${4:-}"
require_nonempty "USER_NAME" "$USER_NAME"
require_integer "PORT" "$PORT"
require_ip "TARGET_IP" "$TARGET_IP"
require_safe_path "TARGET_PATH" "$TARGET_PATH"
echo "validated: $USER_NAME $PORT $TARGET_IP $TARGET_PATH"
命令行参数解析(getopts)与安全执行(禁止 eval):
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 -u user -p port -i ip -f file"
exit 1
}
while getopts ":u:p:i:f:" opt; do
case "$opt" in
u) USER_NAME="$OPTARG" ;;
p) PORT="$OPTARG" ;;
i) TARGET_IP="$OPTARG" ;;
f) FILE_PATH="$OPTARG" ;;
*) usage ;;
esac
done
# 校验
require_nonempty "USER_NAME" "${USER_NAME:-}"
require_integer "PORT" "${PORT:-}"
require_ip "TARGET_IP" "${TARGET_IP:-}"
require_safe_path "FILE_PATH" "${FILE_PATH:-}"
# 安全引用,避免词分割
cp -- "$FILE_PATH" "/tmp/${USER_NAME}.bak"
echo "Copied to /tmp/${USER_NAME}.bak"
外部输入读取(避免转义与空格丢失):
#!/usr/bin/env bash
set -euo pipefail
INPUT_FILE="/tmp/targets.txt"
while IFS= read -r line; do
[[ -z "$line" ]] && continue
require_ip "TARGET_IP" "$line"
echo "ok: $line"
done < "$INPUT_FILE"
结构化输入校验(jq/yq 安装与示例):
# 安装 jq/yq(Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y jq yq
# 安装 jq/yq(CentOS/RHEL)
sudo yum install -y epel-release
sudo yum install -y jq
sudo wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
# config.json 示例
cat >/tmp/config.json <<'EOF'
{"port": 9092, "host": "10.0.0.5"}
EOF
PORT="$(jq -r '.port // empty' /tmp/config.json)"
HOST="$(jq -r '.host // empty' /tmp/config.json)"
require_integer "PORT" "$PORT"
require_ip "HOST" "$HOST"
echo "config ok: $HOST:$PORT"
常见问题与排错:
# 1) 未定义变量报错:set -u
# 解决:使用默认值或先判断
: "${TOKEN:?TOKEN is required}"
# 2) 正则不生效
# 解释:[[ ]] 支持 =~ 正则,需避免在 /bin/sh 中执行
bash -n /path/script.sh # 语法检查
bash -x /path/script.sh # 跟踪执行
# 3) 变量被词分割
# 解决:所有变量使用双引号
cp "$src" "$dst"
练习(建议自测):
1. 编写脚本 safe_copy.sh,要求使用 getopts 接收 -s 源文件和 -d 目标目录,校验路径安全并执行拷贝。
2. 在 targets.txt 中加入非法 IP(如 999.1.1.1),验证脚本能正确报错并退出。
3. 将脚本改为从 JSON 读取 port 与 host,缺失字段必须提示并退出。
4. 设计一个 require_enum 函数,仅允许 start|stop|restart 三种操作。