Skip to content

bug: prevent tmpsh temporary-directory symlink escape#93

Open
ALX99 wants to merge 2 commits into
masterfrom
agent/prevent-tmpsh-temp-symlink
Open

bug: prevent tmpsh temporary-directory symlink escape#93
ALX99 wants to merge 2 commits into
masterfrom
agent/prevent-tmpsh-temp-symlink

Conversation

@ALX99

@ALX99 ALX99 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Bug

.local/bin/tmpsh creates its working directory at the predictable path /tmp/$$.tmpsh with mkdir -p. A local process can pre-create that path as a symlink to another directory before tmpsh starts. mkdir -p accepts a symlink to an existing directory, and cd then follows it, so the interactive shell opens in the attacker-selected directory rather than a private temporary directory.

Severity and impact

This is a local security and correctness bug. On a multi-user system, another user can predict or race the process ID and redirect tmpsh into a directory they control. Even on a single-user system, a stale or deliberately created path can make the command operate in the wrong directory. Commands run in the supposedly disposable shell may therefore read, modify, or delete unrelated files.

Evidence

Relevant implementation on master:

  • .local/bin/tmpsh sets dir=/tmp/$$.tmpsh.
  • It then runs mkdir -p "$dir", which succeeds when that path is already a symlink to an existing directory.
  • cd "$dir" follows the symlink and starts ${SHELL:-/bin/sh} in the symlink target.

The regression test deterministically preserves the wrapper process ID across exec, pre-creates /tmp/$$.tmpsh as a symlink to a directory containing a sentinel file, and uses a non-interactive helper shell to detect whether the sentinel is visible.

On the original implementation, the regression test exits with status 1 after the helper starts in the symlink target. With the fix, the helper starts in a newly created mktemp directory and the test exits successfully.

This is a real defect rather than a hypothetical path-handling concern because the unsafe path is predictable, the symlink is followed deterministically, and no race is required for the included reproduction.

Reproduce

Setup:

git checkout master

Create an attacker-selected directory and helper shell, then preserve the wrapper PID across exec so the symlink name exactly matches the path used by tmpsh:

sandbox=$(mktemp -d)
mkdir "$sandbox/victim"
touch "$sandbox/victim/sentinel"
cat >"$sandbox/helper" <<'EOF'
#!/bin/sh
test ! -e "$PWD/sentinel"
EOF
chmod +x "$sandbox/helper"

sh -c '
  ln -s "$1" "/tmp/$$.tmpsh"
  exec env SHELL="$2" "$3"
' sh "$sandbox/victim" "$sandbox/helper" .local/bin/tmpsh

Actual result on master: the helper starts in $sandbox/victim, sees sentinel, and exits non-zero.

Expected result: tmpsh creates a private, unique temporary directory and never enters the pre-created symlink target.

The committed regression test automates this procedure without adding a standalone repro-only artifact.

Root cause

The script derives a temporary-directory name directly from its process ID and uses mkdir -p rather than atomically creating a new directory. Process IDs are predictable, and mkdir -p does not reject an existing symlink to a directory.

Fix

Replace /tmp/$$.tmpsh plus mkdir -p with:

tmp_root=${TMPDIR:-/tmp}
dir=$(mktemp -d "$tmp_root/tmpsh.XXXXXX")

mktemp -d atomically creates a unique directory and fails rather than reusing an attacker-controlled path. Respecting TMPDIR also uses the caller's configured temporary-file location while preserving /tmp as the fallback. The existing cleanup trap remains unchanged.

Validation

Commands and results:

  • bash tests/tmpsh-secure-tempdir.test.sh against the original implementation: failed with exit status 1, proving that the helper entered the pre-created symlink target.
  • bash tests/tmpsh-secure-tempdir.test.sh against the fixed implementation: passed; the helper did not enter the victim directory and the generated temporary directory was removed.
  • shellcheck .local/bin/tmpsh tests/tmpsh-secure-tempdir.test.sh: not executed because shellcheck is not installed in the available validation environment.

Regression test added: tests/tmpsh-secure-tempdir.test.sh.

Scope

This PR only changes how tmpsh creates its temporary working directory and adds focused regression coverage. It does not alter shell selection, cleanup semantics, command tracing, unrelated scripts, dependencies, or repository architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant