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 读取 porthost,缺失字段必须提示并退出。
4. 设计一个 require_enum 函数,仅允许 start|stop|restart 三种操作。