made a more secure validation script for rsync
This commit is contained in:
@@ -1,18 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
case "${SSH_ORIGINAL_COMMAND}" in
|
||||
*\&*)
|
||||
echo "Rejected 1"
|
||||
;;
|
||||
*\;*)
|
||||
echo "Rejected 2"
|
||||
;;
|
||||
rsync*)
|
||||
${SSH_ORIGINAL_COMMAND} # no quoting here!
|
||||
;;
|
||||
*true*)
|
||||
set -o errexit -o nounset -o noglob -o pipefail
|
||||
|
||||
readonly LOGGER="logger -t validate-rsync.sh -p authpriv.warning"
|
||||
READONLY="${READONLY:-false}" # set true to enforce read-only (--sender required)
|
||||
ALLOWED_DIR="${ALLOWED_DIR:-}" # e.g. "/backup" to restrict absolute paths
|
||||
|
||||
reject() {
|
||||
${LOGGER} "REJECTED from ${SSH_CONNECTION:-unknown}: ${SSH_ORIGINAL_COMMAND}"
|
||||
echo "Rejected: ${*}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -n "${SSH_ORIGINAL_COMMAND:-}" ]] || reject "no command"
|
||||
|
||||
IFS=' ' read -r -a args <<< "${SSH_ORIGINAL_COMMAND}"
|
||||
[[ ${#args[@]} -ge 1 ]] || reject "empty command"
|
||||
|
||||
# Allow exact "true" as heartbeat check
|
||||
if [[ ${#args[@]} -eq 1 && "${args[0]}" == "true" ]]; then
|
||||
echo "${SSH_ORIGINAL_COMMAND}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Must be rsync in server mode
|
||||
[[ ${#args[@]} -ge 2 ]] || reject "too few arguments"
|
||||
[[ "${args[0]}" == "rsync" ]] || reject "must start with rsync"
|
||||
[[ "${args[1]}" == "--server" ]] || reject "must be in server mode"
|
||||
|
||||
validated=("rsync" "--server")
|
||||
|
||||
for ((i = 2; i < ${#args[@]}; i++)); do
|
||||
arg="${args[i]}"
|
||||
|
||||
# Standalone -e or -M (aliases for --rsh / --remote-option)
|
||||
if [[ "${arg}" == "-e" || "${arg}" == "-M" ]]; then
|
||||
reject "standalone ${arg} is not allowed"
|
||||
fi
|
||||
|
||||
# Short option bundle or single short option (e.g. -vlogDtprze.iLsfxC or -v)
|
||||
if [[ "${arg}" =~ ^-[a-zA-Z0-9.]+$ ]]; then
|
||||
for ((j = 1; j < ${#arg}; j++)); do
|
||||
c="${arg:j:1}"
|
||||
[[ "${c}" =~ [-a-zA-Z0-9.] ]] || reject "illegal short option -${c} in ${arg}"
|
||||
done
|
||||
validated+=("${arg}")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Long option (with optional =value)
|
||||
if [[ "${arg}" =~ ^--[a-zA-Z_][a-zA-Z0-9_-]*(\=.+)?$ ]]; then
|
||||
opt="${arg%%=*}"
|
||||
case "${opt}" in
|
||||
--rsh|--rsync-path|--remote-option)
|
||||
reject "${opt} is not allowed"
|
||||
;;
|
||||
--files-from|--password-file|--early-input)
|
||||
reject "${opt} is not allowed"
|
||||
;;
|
||||
--write-batch|--only-write-batch|--read-batch)
|
||||
reject "${opt} is not allowed"
|
||||
;;
|
||||
--copy-as)
|
||||
reject "${opt} is not allowed"
|
||||
;;
|
||||
*)
|
||||
echo "Rejected 3"
|
||||
validated+=("${arg}")
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
# Path argument (relative, absolute, or ~-prefixed)
|
||||
if [[ "${arg}" =~ ^[./~] ]] || [[ "${arg}" =~ ^[[:alnum:]_] ]]; then
|
||||
if [[ -n "${ALLOWED_DIR}" ]]; then
|
||||
resolved="$(realpath -m "${arg}" 2>/dev/null || true)"
|
||||
if [[ -n "${resolved}" && "${resolved}" != "${ALLOWED_DIR}"* ]]; then
|
||||
reject "path ${arg} resolves outside ${ALLOWED_DIR}"
|
||||
fi
|
||||
fi
|
||||
validated+=("${arg}")
|
||||
continue
|
||||
fi
|
||||
|
||||
reject "disallowed argument: ${arg}"
|
||||
done
|
||||
|
||||
# Enforce read-only mode
|
||||
if ${READONLY}; then
|
||||
has_sender=false
|
||||
for v in "${validated[@]}"; do
|
||||
[[ "${v}" == "--sender" ]] && { has_sender=true; break; }
|
||||
done
|
||||
${has_sender} || reject "read-only mode requires --sender"
|
||||
fi
|
||||
|
||||
exec "${validated[@]}"
|
||||
|
||||
Reference in New Issue
Block a user