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=$(prompt_input "Set hostname" "veilor") || return 1
# Reject shell-special chars in passwords. Generated kickstart writes
# them via heredoc + sed substitution; bare $, ", \, ` would corrupt
# the ks line or partially expand. 8-char min for entropy.
# Reject shell-special and sed-special chars in passwords. Generated
# kickstart writes them via heredoc + sed substitution; bare $, ", \, `
# 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() {
local pw=$1 label=$2
if [[ ${#pw} -lt 8 ]]; then
prompt_error "Weak $label — minimum 8 characters."
return 1
fi
if [[ $pw =~ [\"\$\\\`] ]]; then
prompt_error "Invalid $label — cannot contain: \" \$ \\ \`"
if [[ $pw =~ [\"\$\\\`\&\|/$'\n'] ]]; then
prompt_error "Invalid $label — cannot contain: \" \$ \\ \` & | / newline"
return 1
fi
return 0
@ -253,6 +259,17 @@ Proceed?" || return 1
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() {
# Build kickstart for actual disk install.
# NOTE: passwords go in via --plaintext to avoid storing crypted hash
@ -483,15 +500,16 @@ echo "════════════════════════
# Reboot when done
reboot
KSEOF
# Substitute placeholders. Use | as sed delimiter (passwords might
# contain /). Forbidden chars in passwords (validated upstream): "$\`
# — sed safe.
# Substitute placeholders. Use | as sed delimiter. validate_pw()
# already rejects "$\`&|/\n at input — sed_escape() is defence in
# depth in case future code paths feed unsanitised values (e.g.
# locale/hostname from a file, or a relaxed validator).
sed -i \
-e "s|__LOCALE__|$SEL_LOCALE|" \
-e "s|__HOSTNAME__|$SEL_HOSTNAME|" \
-e "s|__DISK_BASENAME__|$disk_basename|" \
-e "s|__LUKS_PW__|$SEL_LUKS_PW|" \
-e "s|__ADMIN_PW__|$SEL_ADMIN_PW|" \
-e "s|__LOCALE__|$(sed_escape "$SEL_LOCALE")|" \
-e "s|__HOSTNAME__|$(sed_escape "$SEL_HOSTNAME")|" \
-e "s|__DISK_BASENAME__|$(sed_escape "$disk_basename")|" \
-e "s|__LUKS_PW__|$(sed_escape "$SEL_LUKS_PW")|" \
-e "s|__ADMIN_PW__|$(sed_escape "$SEL_ADMIN_PW")|" \
"$out"
echo "[INFO] generated kickstart at $out"
return 0