From d0b8678eb5fa91f5a269eefd0a612667213d48ff Mon Sep 17 00:00:00 2001 From: veilor-org Date: Sat, 2 May 2026 04:33:13 +0100 Subject: [PATCH] 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 --- overlay/usr/local/sbin/veilor-installer | 44 +++++++++++++++++-------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/overlay/usr/local/sbin/veilor-installer b/overlay/usr/local/sbin/veilor-installer index d2a0330..f1c7047 100644 --- a/overlay/usr/local/sbin/veilor-installer +++ b/overlay/usr/local/sbin/veilor-installer @@ -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