Skip to content

bug: prevent tmpsh temporary-directory symlink escape - #93

Merged
ALX99 merged 3 commits into
masterfrom
agent/prevent-tmpsh-temp-symlink
Jul 14, 2026
Merged

bug: prevent tmpsh temporary-directory symlink escape#93
ALX99 merged 3 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 created its working directory at predictable /tmp/$$.tmpsh with mkdir -p. A pre-created symlink at that path was accepted and followed, so the shell could start inside an attacker-selected directory instead of a private temporary directory.

Severity and impact

This is a local security and correctness bug. On a multi-user system another process can predict or race the PID-derived path; even on a single-user machine, a stale or deliberate symlink can redirect a supposedly disposable shell into unrelated files.

Evidence

Relevant code: .local/bin/tmpsh.

The parent implementation performed:

dir=/tmp/$$.tmpsh
mkdir -p "$dir"
cd "$dir"

mkdir -p succeeds when the path already resolves through a symlink to an existing directory, and cd follows that symlink. The unsafe behavior is deterministic when the symlink is created before tmpsh starts; no successful race is required.

Reproduce

Use this temporary, uncommitted procedure:

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

The exec preserves the wrapper PID, ensuring the symlink name exactly matches the path used by the parent implementation.

Actual result before the fix: the helper starts in $sandbox/victim, sees sentinel, and exits nonzero.

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

Root cause

The temporary directory name was derived directly from a predictable process ID and was created non-atomically with mkdir -p, which permits reuse of an existing symlink-to-directory path.

Fix

Use:

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

mktemp -d atomically creates a unique directory and refuses to reuse an attacker-controlled path. The existing cleanup trap remains in place.

Validation

The temporary harness above was run before the committed test artifact was removed:

  • Parent implementation: failed with status 1 because the helper entered the symlink target.
  • Fixed implementation: passed; the helper did not enter the victim directory and the generated temporary directory was removed.
  • shellcheck was not run because it was unavailable in the validation environment.

GitHub comparison against master now shows only .local/bin/tmpsh changed (2 additions, 2 deletions). No test or reproduction artifact is included in the PR.

Scope

Only temporary-directory creation changes. Shell selection, cleanup semantics, command tracing, dependencies, and unrelated scripts are intentionally unchanged. No tests, fixtures, snapshots, repro scripts, logs, or evidence-only files are added or modified.

@ALX99
ALX99 merged commit 650fadd into master Jul 14, 2026
4 checks passed
@ALX99
ALX99 deleted the agent/prevent-tmpsh-temp-symlink branch July 14, 2026 11:34
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