From 041356e68b152077c5a4cb255b19ea87cee567c0 Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Sun, 26 Jul 2026 18:24:21 +0200 Subject: [PATCH] fix(templates): escape $ and backtick when writing /.env and /etc/environment dotenv_quote emits a double-quoted value, and both files are read back by sourcing them from a shell: browserless_chrome does "set -a; . /.env" from its Dockerfile, wger copies /.env into /data/env.sh as export lines, and fireflyiii_data_importer relies on /etc/environment for cron. Inside double quotes $ and ` are still special, and neither was escaped, so the value was expanded rather than read literally: pa$$w0rd came back as pa904869w0rd (the shell PID) ${HOME} came back as /root back`tick` ran tick as a command and kept only "back" Escape both, after the existing backslash doubling so the added backslashes are not doubled in turn. Extend --self-test to cover this path as well: it now writes an env file, checks it parses, sources it and compares. An unescaped backtick makes the file unparseable, which would take the sourcing shell down with it, so that case is reported rather than left to abort the run. Values containing a real newline remain out of scope: dotenv_quote writes them as a literal \n, which a dotenv parser unescapes but a shell does not. Co-Authored-By: Claude --- .templates/00-global_var.sh | 60 ++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/.templates/00-global_var.sh b/.templates/00-global_var.sh index 97ed02f3d2..9e111d15f1 100755 --- a/.templates/00-global_var.sh +++ b/.templates/00-global_var.sh @@ -20,10 +20,21 @@ mktemp_safe() { } dotenv_quote() { - # For /.env and /etc/environment: double quotes + minimal escaping + # For /.env and /etc/environment: double quotes + minimal escaping. + # + # These files are read back by sourcing them from a shell, so every + # character that is still special inside double quotes has to be escaped. + # $ and ` used to be left alone, which meant a value was expanded instead of + # being read literally: a password like pa$$w0rd came back with the shell + # PID spliced into it, and a value containing backticks ran as a command. + # + # Backslash must be doubled first, so that the backslashes added below are + # not doubled in turn. local v="$1" v="${v//\\/\\\\}" v="${v//\"/\\\"}" + v="${v//\$/\\\$}" + v="${v//\`/\\\`}" v="${v//$'\n'/\\n}" v="${v//$'\r'/\\r}" printf '"%s"' "$v" @@ -98,30 +109,51 @@ if [[ "${1:-}" == "--self-test" ]]; then JSONSOURCE="self-test" EXPORT_BODY="$(mktemp_safe)" EXPORT_BLOCK="$(mktemp_safe)" - trap 'rm -f "$EXPORT_BODY" "$EXPORT_BLOCK"' EXIT + self_test_env="$(mktemp_safe)" + trap 'rm -f "$EXPORT_BODY" "$EXPORT_BLOCK" "$self_test_env"' EXIT + self_test_rc=0 + self_test_check() { + # $1 name of the variable that was read back, $2 expected value, $3 how + local self_test_got="${!1}" + [[ "$self_test_got" == "$2" ]] && return 0 + printf 'FAIL (%s): <%s> came back as <%s>\n' "$3" "$2" "$self_test_got" + self_test_rc=1 + } + + # 1. The export block, sourced the way an injected run script would for self_test_i in "${!self_test_values[@]}"; do append_export "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}" done compose_export_block - - # Source the generated block the way an injected run script would # shellcheck source=/dev/null . "$EXPORT_BLOCK" - - self_test_rc=0 for self_test_i in "${!self_test_values[@]}"; do - self_test_name="SELFTEST_${self_test_i}" - if [[ "${!self_test_name}" != "${self_test_values[$self_test_i]}" ]]; then - printf 'FAIL: <%s> came back as <%s> via %s\n' \ - "${self_test_values[$self_test_i]}" "${!self_test_name}" \ - "$(shell_quote "${self_test_values[$self_test_i]}")" - self_test_rc=1 - fi + self_test_check "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "export block" done + # 2. /.env, sourced the way browserless_chrome and wger read it back. + # Values holding a newline are out of scope: dotenv_quote writes them as a + # literal \n, which a dotenv parser unescapes but a shell does not. + for self_test_i in "${!self_test_values[@]}"; do + printf 'DOTENVTEST_%s=%s\n' \ + "$self_test_i" "$(dotenv_quote "${self_test_values[$self_test_i]}")" + done > "$self_test_env" + if ! bash -n "$self_test_env" 2>/dev/null; then + # An unescaped backtick or quote leaves the file unparseable, which would + # abort the sourcing shell instead of just yielding a wrong value. + echo "FAIL (dotenv): generated env file is not valid shell" + self_test_rc=1 + else + # shellcheck source=/dev/null + . "$self_test_env" + for self_test_i in "${!self_test_values[@]}"; do + self_test_check "DOTENVTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "dotenv" + done + fi + [[ "$self_test_rc" -eq 0 ]] && - echo "export block: ${#self_test_values[@]} values round-tripped unchanged" + echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)" exit "$self_test_rc" fi