diff --git a/acme.sh b/acme.sh index 20a12992..99d68656 100755 --- a/acme.sh +++ b/acme.sh @@ -2499,6 +2499,15 @@ _send_signed_request() { } +#Reads a value from stdin, prints it escaped for use as the replacement text +#of a sed s command delimited by '|'. The backslash must go first: a bare one +#starts an escape sequence and backslash-digit is a backreference, both make +#sed error out. Then '&' (the whole-match reference) and the '|' delimiter. +#https://github.com/acmesh-official/acme.sh/issues/7213 +_sed_escape_rhs() { + sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g' +} + #setopt "file" "opt" "=" "value" [";"] _setopt() { __conf="$1" @@ -2514,34 +2523,50 @@ _setopt() { touch "$__conf" chmod 600 "$__conf" fi + __nl=" +" + case "$__val" in + *"$__nl"*) + #the conf format is line based and the file is sourced by the shell, so a + #value holding a line break cannot be represented in it (it would also + #make the replace sed below fail with an unterminated 's' command) + _err "The value of '$__opt' contains a line break, it cannot be saved to $__conf." + return 1 + ;; + esac if [ -n "$(_tail_c 1 <"$__conf")" ]; then echo >>"$__conf" fi if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then _debug3 OK - if _contains "$__val" "&"; then - __val="$(echo "$__val" | sed 's/&/\\&/g')" - fi - if _contains "$__val" "|"; then - __val="$(echo "$__val" | sed 's/|/\\|/g')" - fi + __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" text="$(cat "$__conf")" - printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + #capture first, write only on success: redirecting sed straight into the + #conf file truncates it before sed runs, so a failing sed (e.g. on an + #unescaped special character in the value) wiped the whole conf (#2426) + if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then + printf -- "%s\n" "$__text" >"$__conf" + else + _err "Cannot save '$__opt' to $__conf." + return 1 + fi elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then - if _contains "$__val" "&"; then - __val="$(echo "$__val" | sed 's/&/\\&/g')" - fi - if _contains "$__val" "|"; then - __val="$(echo "$__val" | sed 's/|/\\|/g')" - fi + __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" text="$(cat "$__conf")" - printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then + printf -- "%s\n" "$__text" >"$__conf" + else + _err "Cannot save '$__opt' to $__conf." + return 1 + fi else _debug3 APP - echo "$__opt$__sep$__val$__end" >>"$__conf" + #printf, not echo: dash's builtin echo interprets backslash escapes in + #the value and would corrupt it + printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf" fi _debug3 "$(grep -n "^$__opt$__sep" "$__conf")" } @@ -2569,7 +2594,9 @@ _clear_conf() { _sdkey="$2" if [ "$_c_c_f" ]; then _conf_data="$(cat "$_c_c_f")" - echo "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f" + #printf, not echo: dash's builtin echo interprets backslash escapes and + #would corrupt saved values that contain them on every rewrite + printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f" else _err "Config file is empty, cannot clear" fi