diff --git a/start.sh b/start.sh index a1406663..8a37bf40 100755 --- a/start.sh +++ b/start.sh @@ -40,10 +40,14 @@ if [[ -f "${REPO_ROOT}/.env" ]]; then # both `source` and `.env`. # Sourced from PR #1686 (@binhpt310) — Cluster 1 (operational hardening), # extracted to a focused follow-up after the parent PR was deferred. + _hermes_env_filtered="$(mktemp "${TMPDIR:-/tmp}/hermes-webui-env.XXXXXX")" + grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' "${REPO_ROOT}/.env" > "${_hermes_env_filtered}" || true set -a # shellcheck source=/dev/null - source <(grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' "${REPO_ROOT}/.env") + source "${_hermes_env_filtered}" set +a + rm -f "${_hermes_env_filtered}" + unset _hermes_env_filtered fi PYTHON="${HERMES_WEBUI_PYTHON:-}" diff --git a/tests/test_docker_env_readonly_vars.py b/tests/test_docker_env_readonly_vars.py index 226f0b8d..42d07dd1 100644 --- a/tests/test_docker_env_readonly_vars.py +++ b/tests/test_docker_env_readonly_vars.py @@ -66,23 +66,16 @@ class TestStartShReadonlyEnvFilter: f"'{var}: readonly variable'" ) - def test_filter_pattern_uses_grep_or_equivalent(self): + def test_filter_pattern_uses_grep_before_source(self): """Filter must use a pattern that strips readonly-var lines before - the bash `source` consumes them. `grep -vE` is the canonical form; - the assertion accepts any process-substitution-into-source shape.""" - # Look for `source <(...UID...)` pattern. Note that the inner shell - # expression can contain its own parens (e.g. `(export[[:space:]]+)`), - # so we use a non-greedy `.*?` rather than `[^)]*`. - assert re.search( - r"source\s+<\(.*?UID.*?\)", - START_SH, - re.DOTALL, - ), ( - "start.sh's .env loader must filter readonly bash vars " - "(UID/GID/EUID/EGID/PPID) via `source <(grep -vE ...)` or " - "equivalent process-substitution form before `source`-ing " - "the .env file" - ) + the bash `source` consumes them. The loader may use a temporary file + rather than process substitution because some bash/macOS combinations + can source an empty `/dev/fd/*` stream from `source <(grep ...)`.""" + grep_idx = START_SH.find("grep -vE") + source_idx = START_SH.find("source", grep_idx) + assert grep_idx != -1, "start.sh must filter readonly vars with grep -vE or equivalent" + assert source_idx != -1, "start.sh must source the filtered .env stream" + assert grep_idx < source_idx, "readonly-var filtering must happen before source" def test_filter_handles_optional_export_prefix(self): """The ``export`` prefix on env vars is optional but common. The