Never truncate the conf file when a saved value breaks the rewrite sed

A value holding a backslash-digit sequence (a backreference to sed) or an
embedded line break made _setopt's replace command fail after the shell
had already truncated the conf file, wiping the whole domain conf; the
next renewal then fails with an empty Le_API and no validation method.
Same class as #2426, which escaped only '&' and '|'.

Escape the backslash too, write the sed output back only when sed
succeeds, reject values holding a line break, and rewrite the file with
printf instead of echo in the append path and in _clear_conf: dash's
builtin echo interprets backslash escapes and corrupted such values on
every rewrite.

https://github.com/acmesh-official/acme.sh/issues/7213
This commit is contained in:
neil
2026-08-30 16:02:57 +08:00 Unverified
parent 6e1deacac0
commit 4756183e30
+43 -16
View File
@@ -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