From 637b8be290c5d109452f1163ed08958b5007059c Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Tue, 27 Oct 2026 12:38:42 -0500 Subject: [PATCH 1/4] feat(minimal): support *_FILE variables for Docker/Kubernetes secrets Adds file_env to the minimal entrypoint so credentials (DB setup/app passwords, CSRF secret, mail and admin credentials) can be read from a file such as /run/secrets/ instead of the environment. Setting both VAR and VAR_FILE, or pointing at an unreadable file, is fatal. Refs #83 Co-Authored-By: Claude Sonnet 5.5 --- simplerisk-minimal/README.md | 27 ++++++++++++++++++ simplerisk-minimal/common/entrypoint.sh | 37 +++++++++++++++++++++++++ simplerisk-minimal/test_file_env.sh | 29 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100755 simplerisk-minimal/test_file_env.sh diff --git a/simplerisk-minimal/README.md b/simplerisk-minimal/README.md index e8c6931..5ad0bfa 100644 --- a/simplerisk-minimal/README.md +++ b/simplerisk-minimal/README.md @@ -80,6 +80,33 @@ docker run -d --name simplerisk -e SIMPLERISK_DB_PASSWORD=pass -e SIMPLERISK_DB_ | `ADMIN_EMAIL` | — | Email address for the initial admin user | | `ADMIN_NAME` | `Administrator` | Display name for the initial admin user | +## Secrets (`_FILE` variables) + +To keep credentials out of the container environment (and out of compose files), the following variables can instead be supplied as a path to a file, by appending `_FILE` to the name. This works with Docker secrets (`/run/secrets/`), Kubernetes secret mounts, or any mounted file. The entrypoint reads the file, trailing newlines are stripped, and the `_FILE` variable is removed before Apache starts. + +`DB_SETUP_USER`, `DB_SETUP_PASS`, `SIMPLERISK_DB_HOSTNAME`, `SIMPLERISK_DB_PORT`, `SIMPLERISK_DB_USERNAME`, `SIMPLERISK_DB_PASSWORD`, `SIMPLERISK_DB_DATABASE`, `SIMPLERISK_CSRF_SECRET`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `ADMIN_USERNAME`, `ADMIN_PASSWORD` + +Setting both `VAR` and `VAR_FILE` is an error, as is a `_FILE` path that is not a readable file; the container exits with a message. + +```yaml +services: + simplerisk: + image: simplerisk/simplerisk-minimal + environment: + - DB_SETUP=automatic + - DB_SETUP_PASS_FILE=/run/secrets/dbms_root_pass + - SIMPLERISK_DB_PASSWORD_FILE=/run/secrets/db_pass + secrets: + - dbms_root_pass + - db_pass + +secrets: + dbms_root_pass: + file: ./dbms_root_pass.txt + db_pass: + file: ./db_pass.txt +``` + ## Mail settings The following environment variables configure the outbound mail settings stored in the SimpleRisk database (`settings` table). They are applied on every container start, so changing a value and restarting the container is all that is needed to update the configuration. Invalid values are silently skipped with a warning in the container logs. diff --git a/simplerisk-minimal/common/entrypoint.sh b/simplerisk-minimal/common/entrypoint.sh index cf43d40..84e8d2a 100644 --- a/simplerisk-minimal/common/entrypoint.sh +++ b/simplerisk-minimal/common/entrypoint.sh @@ -32,6 +32,41 @@ fatal_error(){ exit 1 } +# Docker/Kubernetes secrets support: if VAR_FILE is set, read VAR from the file it +# points to (typically /run/secrets/), so credentials need not live in the +# container's environment. Setting both VAR and VAR_FILE is an error. The _FILE +# variable is unset afterwards so it is not inherited by child processes. +file_env() { + local var="$1" + local file_var="${var}_FILE" + local var_val="${!var:-}" + local file_val="${!file_var:-}" + + if [ -n "$var_val" ] && [ -n "$file_val" ]; then + fatal_error "Both $var and $file_var are set (but are exclusive)." + fi + + if [ -n "$file_val" ]; then + [ -r "$file_val" ] || fatal_error "$file_var points to '$file_val', which is not a readable file." + # $(< file) strips trailing newlines, which secret files commonly end with + export "$var=$(< "$file_val")" + fi + unset "$file_var" +} + +load_file_envs() { + local var + for var in \ + DB_SETUP_USER DB_SETUP_PASS \ + SIMPLERISK_DB_HOSTNAME SIMPLERISK_DB_PORT SIMPLERISK_DB_USERNAME \ + SIMPLERISK_DB_PASSWORD SIMPLERISK_DB_DATABASE \ + SIMPLERISK_CSRF_SECRET \ + MAIL_USERNAME MAIL_PASSWORD \ + ADMIN_USERNAME ADMIN_PASSWORD; do + file_env "$var" + done +} + set_db_password(){ if [[ "${DB_SETUP:-}" = automatic* ]]; then # shellcheck disable=SC2015 @@ -470,6 +505,8 @@ unset_variables() { } _main() { + load_file_envs + set_ssl_certificate # Detect whether the operator has opted into Docker-managed config diff --git a/simplerisk-minimal/test_file_env.sh b/simplerisk-minimal/test_file_env.sh new file mode 100755 index 0000000..79d4e94 --- /dev/null +++ b/simplerisk-minimal/test_file_env.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Regression checks for the *_FILE secrets support in common/entrypoint.sh. +set -uo pipefail +cd "$(dirname "$(readlink -f "$0")")" + +fail=0 +ok() { echo "ok: $1"; } +bad() { echo "FAIL: $1"; fail=1; } + +# Load only the helpers under test; fatal_error is stubbed to exit the subshell. +helpers=$(sed -n '/^file_env() {/,/^}/p;/^load_file_envs() {/,/^}/p' common/entrypoint.sh) +run() { bash -c 'set -eo pipefail; fatal_error(){ echo "FATAL: $1"; exit 1; }; '"$helpers"'; '"$1"; } + +dir=$(mktemp -d); trap 'rm -rf "$dir"' EXIT +printf 'p@ss w/&|\n' > "$dir/secret" + +out=$(DB_SETUP_PASS_FILE="$dir/secret" run 'load_file_envs; [ "$DB_SETUP_PASS" = "p@ss w/&|" ] && [ -z "${DB_SETUP_PASS_FILE:-}" ] && echo pass') +[ "$out" = pass ] && ok "reads file, strips newline, unsets _FILE" || bad "reads file (got: $out)" + +SIMPLERISK_DB_PASSWORD=x SIMPLERISK_DB_PASSWORD_FILE="$dir/secret" run 'load_file_envs' >/dev/null \ + && bad "VAR and VAR_FILE together should be rejected" || ok "VAR and VAR_FILE together rejected" + +MAIL_PASSWORD_FILE="$dir/missing" run 'load_file_envs' >/dev/null \ + && bad "unreadable _FILE should be rejected" || ok "unreadable _FILE rejected" + +SIMPLERISK_DB_PASSWORD=keep run 'load_file_envs; [ "$SIMPLERISK_DB_PASSWORD" = keep ]' \ + && ok "plain VAR untouched when no _FILE" || bad "plain VAR changed" + +exit $fail From 82589dbf05d71df86b7f4982f2af422cfd35bb32 Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Tue, 27 Oct 2026 12:38:56 -0500 Subject: [PATCH 2/4] ci: run the *_FILE secrets test in container-validation Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/container-validation.yml | 3 +++ simplerisk-minimal/test_file_env.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/container-validation.yml b/.github/workflows/container-validation.yml index 094b290..49306ce 100644 --- a/.github/workflows/container-validation.yml +++ b/.github/workflows/container-validation.yml @@ -104,6 +104,9 @@ jobs: - name: simplerisk (full-stack) generator run: ./simplerisk/test_generate_dockerfile.sh + - name: simplerisk-minimal *_FILE secrets support + run: ./simplerisk-minimal/test_file_env.sh + - name: Fail if a generator left the committed Dockerfile modified run: | set -euo pipefail diff --git a/simplerisk-minimal/test_file_env.sh b/simplerisk-minimal/test_file_env.sh index 79d4e94..d4fa6ba 100755 --- a/simplerisk-minimal/test_file_env.sh +++ b/simplerisk-minimal/test_file_env.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Regression checks for the *_FILE secrets support in common/entrypoint.sh. set -uo pipefail -cd "$(dirname "$(readlink -f "$0")")" +cd "$(dirname "$(readlink -f "$0")")" || exit 1 fail=0 ok() { echo "ok: $1"; } From ae9003f20e6d68859c42cdac9403f12525dfdc9e Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Tue, 27 Oct 2026 13:03:37 -0500 Subject: [PATCH 3/4] fix(minimal): neutralise metacharacters in credentials reaching shell, SQL, PHP and sed Credential values were interpolated into a bash -c string (mysql setup/delete), a double-quoted PHP string (csrf-secret.php) and unescaped PHP single-quoted strings (config.php); sed_escape also broke on newlines. _FILE secrets from a secret manager commonly contain these characters. - run mysql via an argv array with the password in MYSQL_PWD, SQL on stdin, string values single-quoted and identifiers quoted with proper escaping - write csrf-secret.php and config.php defines as escaped PHP single-quoted strings - rewrite sed_escape with parameter expansion so newlines survive - pass the app DB password to apply_mail_setting via MYSQL_PWD, not argv - add test_escaping.sh and run it in container-validation Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/container-validation.yml | 3 + simplerisk-minimal/common/entrypoint.sh | 114 +++++++++++++++------ simplerisk-minimal/test_escaping.sh | 50 +++++++++ 3 files changed, 134 insertions(+), 33 deletions(-) create mode 100755 simplerisk-minimal/test_escaping.sh diff --git a/.github/workflows/container-validation.yml b/.github/workflows/container-validation.yml index 49306ce..8242a7d 100644 --- a/.github/workflows/container-validation.yml +++ b/.github/workflows/container-validation.yml @@ -107,6 +107,9 @@ jobs: - name: simplerisk-minimal *_FILE secrets support run: ./simplerisk-minimal/test_file_env.sh + - name: simplerisk-minimal credential escaping + run: ./simplerisk-minimal/test_escaping.sh + - name: Fail if a generator left the committed Dockerfile modified run: | set -euo pipefail diff --git a/simplerisk-minimal/common/entrypoint.sh b/simplerisk-minimal/common/entrypoint.sh index 84e8d2a..2db841a 100644 --- a/simplerisk-minimal/common/entrypoint.sh +++ b/simplerisk-minimal/common/entrypoint.sh @@ -20,11 +20,59 @@ generate_random_password() { } # Escape a value for use as the replacement in a sed s|...|VALUE|... command. -# Escapes \, &, and | so they aren't interpreted as sed metacharacters. -# Required for any env-var-derived value that may contain / (which would -# otherwise terminate the / delimiter the sed substitution used to use). +# Escapes \, &, | and newlines so they aren't interpreted as sed metacharacters +# or terminate the s command. Required for any env-var-derived value that may +# contain / (which would otherwise terminate the / delimiter the sed +# substitution used to use). Done with parameter expansion (not a sed pipe) so +# embedded newlines survive. sed_escape() { - printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g' + local v="$1" + v="${v//\\/\\\\}" + v="${v//&/\\&}" + v="${v//|/\\|}" + v="${v//$'\n'/\\$'\n'}" + printf '%s' "$v" +} + +# Escape a value for a PHP single-quoted string literal ('...'): only \ and ' +# are special there, so a value can neither close the string nor inject code. +php_sq_escape() { + local v="$1" + v="${v//\\/\\\\}" + v="${v//\'/\\\'}" + printf '%s' "$v" +} + +# Escape a value for a MySQL single-quoted string literal ('...'). +sql_str_escape() { + local v="$1" + v="${v//\\/\\\\}" + v="${v//\'/\\\'}" + printf '%s' "$v" +} + +# Escape a value for a MySQL ANSI_QUOTES identifier ("..."). +sql_ident_escape() { + local v="$1" + printf '%s' "${v//\"/\"\"}" +} + +# Rewrite define('NAME', '...'); in $CONFIG_PATH. The value is PHP-escaped first +# (so it cannot break out of the string in config.php) and then sed-escaped. +config_define() { + local name="$1" value="$2" + sed -i "s|\('${name}', '\).*\(');\)|\1$(sed_escape "$(php_sq_escape "$value")")\2|g" "$CONFIG_PATH" +} + +# Run the mysql client as the privileged setup user, reading SQL from stdin. +# Arguments are passed as an argv array (never through a shell string), and the +# password via MYSQL_PWD, so no value is subject to shell interpretation. +mysql_setup() { + local -a flags=() + # DB_SSL_FLAGS is an internal constant ("--ssl-mode=... --enable-cleartext-plugin") + [ -n "${DB_SSL_FLAGS:-}" ] && read -ra flags <<< "$DB_SSL_FLAGS" + MYSQL_PWD="$DB_SETUP_PASS" mysql ${flags[@]+"${flags[@]}"} \ + -u "$DB_SETUP_USER" -h "$SIMPLERISK_DB_HOSTNAME" -P "$SIMPLERISK_DB_PORT" } fatal_error(){ @@ -74,7 +122,7 @@ set_db_password(){ else SIMPLERISK_DB_PASSWORD=${SIMPLERISK_DB_PASSWORD:-simplerisk} fi - sed -i "s|\('DB_PASSWORD', '\).*\(');\)|\1$(sed_escape "$SIMPLERISK_DB_PASSWORD")\2|g" "$CONFIG_PATH" + config_define DB_PASSWORD "$SIMPLERISK_DB_PASSWORD" } validate_db_setup(){ @@ -128,28 +176,28 @@ set_config(){ # Replacing config variables. Values are run through sed_escape so paths # or other inputs containing \, &, or | don't break the substitution. SIMPLERISK_DB_HOSTNAME=${SIMPLERISK_DB_HOSTNAME:-localhost} - sed -i "s|\('DB_HOSTNAME', '\).*\(');\)|\1$(sed_escape "$SIMPLERISK_DB_HOSTNAME")\2|g" "$CONFIG_PATH" + config_define DB_HOSTNAME "$SIMPLERISK_DB_HOSTNAME" SIMPLERISK_DB_PORT=${SIMPLERISK_DB_PORT:-3306} - sed -i "s|\('DB_PORT', '\).*\(');\)|\1$(sed_escape "$SIMPLERISK_DB_PORT")\2|g" "$CONFIG_PATH" + config_define DB_PORT "$SIMPLERISK_DB_PORT" SIMPLERISK_DB_USERNAME=${SIMPLERISK_DB_USERNAME:-simplerisk} - sed -i "s|\('DB_USERNAME', '\).*\(');\)|\1$(sed_escape "$SIMPLERISK_DB_USERNAME")\2|g" "$CONFIG_PATH" + config_define DB_USERNAME "$SIMPLERISK_DB_USERNAME" set_db_password SIMPLERISK_DB_DATABASE=${SIMPLERISK_DB_DATABASE:-simplerisk} - sed -i "s|\('DB_DATABASE', '\).*\(');\)|\1$(sed_escape "$SIMPLERISK_DB_DATABASE")\2|g" "$CONFIG_PATH" + config_define DB_DATABASE "$SIMPLERISK_DB_DATABASE" SIMPLERISK_DB_FOR_SESSIONS=${SIMPLERISK_DB_FOR_SESSIONS:-true} - sed -i "s|\('USE_DATABASE_FOR_SESSIONS', '\).*\(');\)|\1$(sed_escape "$SIMPLERISK_DB_FOR_SESSIONS")\2|g" "$CONFIG_PATH" + config_define USE_DATABASE_FOR_SESSIONS "$SIMPLERISK_DB_FOR_SESSIONS" if [ -n "${SIMPLERISK_DB_SSL_CERT_PATH:-}" ]; then # The sample ships DB_SSL_CERTIFICATE_PATH as a commented-out line so # that operators who don't set the env var don't end up requesting SSL # against a non-existent cert path. When a path is supplied, rewrite # the whole line (commented or not) as an active define. - escaped_ssl_path=$(sed_escape "$SIMPLERISK_DB_SSL_CERT_PATH") + escaped_ssl_path=$(sed_escape "$(php_sq_escape "$SIMPLERISK_DB_SSL_CERT_PATH")") sed -i "s|^[[:space:]]*\(//[[:space:]]*\)\{0,1\}define('DB_SSL_CERTIFICATE_PATH', '[^']*');|define('DB_SSL_CERTIFICATE_PATH', '${escaped_ssl_path}');|" "$CONFIG_PATH" fi @@ -234,7 +282,9 @@ set_csrf_secret(){ CSRF_SECRET_PATH='/var/www/simplerisk/vendor/simplerisk/csrf-magic/csrf-secret.php' # If a SIMPLERISK_CSRF_SECRET value was specified create the csrf-secret.php file with that value - [ -n "${SIMPLERISK_CSRF_SECRET:-}" ] && echo "" > "$CSRF_SECRET_PATH"; + # (single-quoted PHP string, escaped, so the value cannot inject code or be interpolated) + # shellcheck disable=SC2015 + [ -n "${SIMPLERISK_CSRF_SECRET:-}" ] && printf "\n" "$(php_sq_escape "$SIMPLERISK_CSRF_SECRET")" > "$CSRF_SECRET_PATH" || true } set_ssl_certificate(){ @@ -285,8 +335,7 @@ apply_mail_setting(){ # Escape backslashes then single quotes for a MySQL single-quoted string literal local escaped escaped=$(printf '%s' "$value" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g") - mysql -u "$SIMPLERISK_DB_USERNAME" \ - -p"$SIMPLERISK_DB_PASSWORD" \ + MYSQL_PWD="$SIMPLERISK_DB_PASSWORD" mysql -u "$SIMPLERISK_DB_USERNAME" \ -h "$SIMPLERISK_DB_HOSTNAME" \ -P "$SIMPLERISK_DB_PORT" \ --ssl-mode=DISABLED \ @@ -387,16 +436,13 @@ delete_db(){ print_log "db_deletion: prepare" "Performing database deletion" # Pass password via env var to avoid shell interpretation of special characters in the value - export MYSQL_PWD="$DB_SETUP_PASS" - # Needed to separate the GRANT statement from the rest because it was providing a syntax error - exec_cmd "mysql ${DB_SSL_FLAGS:-} -u $DB_SETUP_USER -h$SIMPLERISK_DB_HOSTNAME -P$SIMPLERISK_DB_PORT <= 5 and GNU sed (as in the image and ubuntu CI runners); macOS +# /bin/bash 3.2 and BSD sed give false failures, so run it inside the image there. +set -uo pipefail +cd "$(dirname "$(readlink -f "$0")")" || exit 1 + +fail=0 +ok() { echo "ok: $1"; } +bad() { echo "FAIL: $1"; fail=1; } + +work=$(mktemp -d); trap 'rm -rf "$work"' EXIT + +# Pull in only the helpers under test. +helpers=$(sed -n '/^sed_escape() {/,/^}/p;/^php_sq_escape() {/,/^}/p;/^sql_str_escape() {/,/^}/p;/^sql_ident_escape() {/,/^}/p;/^config_define() {/,/^}/p;/^set_csrf_secret(){/,/^}/p' common/entrypoint.sh \ + | sed "s|/var/www/simplerisk/vendor/simplerisk/csrf-magic/csrf-secret.php|$work/csrf-secret.php|") +eval "$helpers" + +# A value hitting every metacharacter class: " ' \ $ ` & | newline and $(cmd). +nasty=$'a"b\'c\\d$(touch '"$work"$'/PWNED)`touch '"$work"$'/PWNED2`&e|f\ng' + +# --- sed_escape: round-trips through a real sed substitution, newline included +printf 'X\n' > "$work/in" +sed -i "s|X|$(sed_escape "$nasty")|" "$work/in" +[ "$(cat "$work/in")" = "$nasty" ] && ok "sed_escape round-trips nasty value" || bad "sed_escape round-trip" + +# --- SQL escapers +[ "$(sql_str_escape "a'b\\c")" = "a\\'b\\\\c" ] && ok "sql_str_escape" || bad "sql_str_escape" +[ "$(sql_ident_escape 'a"b')" = 'a""b' ] && ok "sql_ident_escape" || bad "sql_ident_escape" + +# --- PHP: config.php define and csrf-secret.php must evaluate back to the exact value +if command -v php >/dev/null 2>&1; then + CONFIG_PATH="$work/config.php" + printf " "$CONFIG_PATH" + config_define DB_PASSWORD "$nasty" + got=$(php -r "include '$CONFIG_PATH'; echo DB_PASSWORD;") + [ "$got" = "$nasty" ] && ok "config_define yields exact value in PHP" || bad "config_define (got: $got)" + + SIMPLERISK_CSRF_SECRET="$nasty" set_csrf_secret + got=$(php -r "include '$work/csrf-secret.php'; echo \$secret;") + [ "$got" = "$nasty" ] && ok "set_csrf_secret yields exact value in PHP" || bad "set_csrf_secret (got: $got)" +else + echo "skip: php not installed (PHP round-trip checks)" +fi + +# --- nothing in the nasty value may have been executed +[ ! -e "$work/PWNED" ] && [ ! -e "$work/PWNED2" ] && ok "no command substitution executed" || bad "injected command executed" + +exit $fail From 3a8f83e5fc2450948a91c3273520f19f839fb28b Mon Sep 17 00:00:00 2001 From: Josh Sokol Date: Tue, 27 Oct 2026 13:18:09 -0500 Subject: [PATCH 4/4] fix(minimal): apply mail settings on MySQL 8 by not forcing a non-TLS connection apply_mail_setting passed --ssl-mode=DISABLED, so MySQL 8's caching_sha2_password refused the app user's first login ("Authentication requires secure connection") and every mail setting was skipped with only a warning. Use the client default (PREFERRED), and escape values with sql_str_escape. Add a regression check. Co-Authored-By: Claude Sonnet 5.5 --- simplerisk-minimal/common/entrypoint.sh | 8 +++++--- simplerisk-minimal/test_escaping.sh | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/simplerisk-minimal/common/entrypoint.sh b/simplerisk-minimal/common/entrypoint.sh index 2db841a..a46573c 100644 --- a/simplerisk-minimal/common/entrypoint.sh +++ b/simplerisk-minimal/common/entrypoint.sh @@ -332,13 +332,15 @@ set_cron(){ apply_mail_setting(){ local db_key="$1" value="$2" - # Escape backslashes then single quotes for a MySQL single-quoted string literal local escaped - escaped=$(printf '%s' "$value" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g") + escaped=$(sql_str_escape "$value") + # No --ssl-mode override: the client's default (PREFERRED) negotiates TLS when the + # server offers it. Forcing DISABLED made MySQL 8's caching_sha2_password refuse the + # first login ("Authentication requires secure connection"), silently skipping + # every mail setting that needs the app user's password. MYSQL_PWD="$SIMPLERISK_DB_PASSWORD" mysql -u "$SIMPLERISK_DB_USERNAME" \ -h "$SIMPLERISK_DB_HOSTNAME" \ -P "$SIMPLERISK_DB_PORT" \ - --ssl-mode=DISABLED \ "$SIMPLERISK_DB_DATABASE" \ -e "UPDATE settings SET value='${escaped}' WHERE name='${db_key}';" \ || print_log "mail_settings:warn" "Failed to update ${db_key}" diff --git a/simplerisk-minimal/test_escaping.sh b/simplerisk-minimal/test_escaping.sh index fa6ef9e..67c7040 100755 --- a/simplerisk-minimal/test_escaping.sh +++ b/simplerisk-minimal/test_escaping.sh @@ -44,6 +44,13 @@ else echo "skip: php not installed (PHP round-trip checks)" fi +# --- apply_mail_setting must not force a non-TLS connection (breaks caching_sha2_password) +if sed -n '/^apply_mail_setting(){/,/^}/p' common/entrypoint.sh | grep -q -- '--ssl-mode=DISABLED'; then + bad "apply_mail_setting forces --ssl-mode=DISABLED" +else + ok "apply_mail_setting does not force --ssl-mode=DISABLED" +fi + # --- nothing in the nasty value may have been executed [ ! -e "$work/PWNED" ] && [ ! -e "$work/PWNED2" ] && ok "no command substitution executed" || bad "injected command executed"