fix: escape sed special chars + reject & | / in passwords

Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes
aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched
pattern. Two layers of defense:
1. validate_pw rejects & | / newline at input
2. sed_escape() helper escapes any remaining special chars before
   substitution
This commit is contained in:
veilor-org 2026-05-02 04:33:13 +01:00
parent 46dc615c8e
commit d0b8678eb5

View file

@ -202,17 +202,23 @@ collect_answers() {
# ── Hostname ── # ── Hostname ──
hostname=$(prompt_input "Set hostname" "veilor") || return 1 hostname=$(prompt_input "Set hostname" "veilor") || return 1
# Reject shell-special chars in passwords. Generated kickstart writes # Reject shell-special and sed-special chars in passwords. Generated
# them via heredoc + sed substitution; bare $, ", \, ` would corrupt # kickstart writes them via heredoc + sed substitution; bare $, ", \, `
# the ks line or partially expand. 8-char min for entropy. # would corrupt the ks line or partially expand at heredoc time.
# &, |, /, newline are sed-special: & expands to the matched pattern
# (so `aA1!@#%^&*()` becomes `aA1!@#%^__ADMIN_PW__*()`), | is our
# delimiter, / would match if delimiter changes, newline breaks the
# sed expression. sed_escape() below adds defense-in-depth, but we
# also reject these at input so the user sees an immediate error
# rather than a corrupted ks file. 8-char min for entropy.
validate_pw() { validate_pw() {
local pw=$1 label=$2 local pw=$1 label=$2
if [[ ${#pw} -lt 8 ]]; then if [[ ${#pw} -lt 8 ]]; then
prompt_error "Weak $label — minimum 8 characters." prompt_error "Weak $label — minimum 8 characters."
return 1 return 1
fi fi
if [[ $pw =~ [\"\$\\\`] ]]; then if [[ $pw =~ [\"\$\\\`\&\|/$'\n'] ]]; then
prompt_error "Invalid $label — cannot contain: \" \$ \\ \`" prompt_error "Invalid $label — cannot contain: \" \$ \\ \` & | / newline"
return 1 return 1
fi fi
return 0 return 0
@ -253,6 +259,17 @@ Proceed?" || return 1
return 0 return 0
} }
# sed_escape — escape sed special chars in a replacement string.
# Replacement-side metacharacters: & (matched pattern), \ (escape),
# | (our chosen delimiter), / (alternate delimiter — escape too in case
# delimiter ever changes). Newline is rejected in validate_pw because
# escaping it portably across BSD/GNU sed is fiddly.
# Order matters: \ must be escaped FIRST so we don't double-escape the
# backslashes we're about to emit for &, |, /.
sed_escape() {
printf '%s' "$1" | sed -e 's/[\\&|/]/\\&/g'
}
generate_ks() { generate_ks() {
# Build kickstart for actual disk install. # Build kickstart for actual disk install.
# NOTE: passwords go in via --plaintext to avoid storing crypted hash # NOTE: passwords go in via --plaintext to avoid storing crypted hash
@ -483,15 +500,16 @@ echo "════════════════════════
# Reboot when done # Reboot when done
reboot reboot
KSEOF KSEOF
# Substitute placeholders. Use | as sed delimiter (passwords might # Substitute placeholders. Use | as sed delimiter. validate_pw()
# contain /). Forbidden chars in passwords (validated upstream): "$\` # already rejects "$\`&|/\n at input — sed_escape() is defence in
# — sed safe. # depth in case future code paths feed unsanitised values (e.g.
# locale/hostname from a file, or a relaxed validator).
sed -i \ sed -i \
-e "s|__LOCALE__|$SEL_LOCALE|" \ -e "s|__LOCALE__|$(sed_escape "$SEL_LOCALE")|" \
-e "s|__HOSTNAME__|$SEL_HOSTNAME|" \ -e "s|__HOSTNAME__|$(sed_escape "$SEL_HOSTNAME")|" \
-e "s|__DISK_BASENAME__|$disk_basename|" \ -e "s|__DISK_BASENAME__|$(sed_escape "$disk_basename")|" \
-e "s|__LUKS_PW__|$SEL_LUKS_PW|" \ -e "s|__LUKS_PW__|$(sed_escape "$SEL_LUKS_PW")|" \
-e "s|__ADMIN_PW__|$SEL_ADMIN_PW|" \ -e "s|__ADMIN_PW__|$(sed_escape "$SEL_ADMIN_PW")|" \
"$out" "$out"
echo "[INFO] generated kickstart at $out" echo "[INFO] generated kickstart at $out"
return 0 return 0