From 0d1948dcc646722b51d195100cadea6b138eb22a Mon Sep 17 00:00:00 2001 From: Dilee Date: Mon, 30 Mar 2026 17:59:48 +0300 Subject: [PATCH] fix(security): run gateway container as non-root and remove shell=True in cleanup Dockerfile ran the entire agent process as root with no USER directive, maximising the blast radius of any container escape. - Create a dedicated hermes user (uid/gid 1000) and switch to it before the entrypoint; ownership of /opt/hermes and /opt/data is transferred - Replace the two shell=True + f-string subprocess.Popen calls in DockerEnvironment.cleanup() with list-form equivalents; eliminates the same shell-injection pattern that was fixed in #1241 and removes the implicit dependency on a POSIX shell in the cleanup path --- Dockerfile | 7 +++++++ tools/environments/docker.py | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 61b725d397c3..32770d124db7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,13 @@ RUN npm install WORKDIR /opt/hermes RUN chmod +x /opt/hermes/docker/entrypoint.sh +RUN groupadd --gid 1000 hermes && \ + useradd --uid 1000 --gid 1000 --shell /bin/bash --create-home hermes && \ + chown -R hermes:hermes /opt/hermes +RUN mkdir -p /opt/data && chown hermes:hermes /opt/data + +USER hermes:hermes + ENV HERMES_HOME=/opt/data VOLUME [ "/opt/data" ] ENTRYPOINT [ "/opt/hermes/docker/entrypoint.sh" ] \ No newline at end of file diff --git a/tools/environments/docker.py b/tools/environments/docker.py index 2a7bb625512f..4de16cf376c7 100644 --- a/tools/environments/docker.py +++ b/tools/environments/docker.py @@ -508,22 +508,27 @@ def _drain(): def cleanup(self): """Stop and remove the container. Bind-mount dirs persist if persistent=True.""" if self._container_id: + container_id = self._container_id try: - # Stop in background so cleanup doesn't block - stop_cmd = ( - f"(timeout 60 {self._docker_exe} stop {self._container_id} || " - f"{self._docker_exe} rm -f {self._container_id}) >/dev/null 2>&1 &" + # Stop in background so cleanup doesn't block. + # Use list-form subprocess to avoid shell injection risk. + subprocess.Popen( + [self._docker_exe, "stop", container_id], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, ) - subprocess.Popen(stop_cmd, shell=True) except Exception as e: - logger.warning("Failed to stop container %s: %s", self._container_id, e) + logger.warning("Failed to stop container %s: %s", container_id, e) if not self._persistent: - # Also schedule removal (stop only leaves it as stopped) + # Schedule removal after stop completes. try: subprocess.Popen( - f"sleep 3 && {self._docker_exe} rm -f {self._container_id} >/dev/null 2>&1 &", - shell=True, + [self._docker_exe, "rm", "-f", container_id], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, ) except Exception: pass