oss-sec mailing list archives
Emacs zero-click local command execution via TRAMP
From: Sean Whitton <spwhitton () spwhitton name>
Date: Fri, 21 Aug 2026 14:33:59 +0100
Bas Alberts of the GitHub Security Lab discovered a local command
execution vulnerability in GNU Emacs 30.2 onwards, and possibly earlier.
I am attaching a fix prepared by Michael Albinus, the TRAMP maintainer.
Here is Bas's description of the problem (edited down a bit).
--8<---------------cut here---------------start------------->8---
1. Tramp: local shell command injection through the user field
(tramp-sh.el, CRITICAL)
tramp-maybe-open-connection builds the login command by expanding
tramp-login-args (%u, %h, ...) and joining the words with
string-join, unquoted, into a command line that is sent to a live
LOCAL shell (tramp-encoding-shell, i.e. /bin/sh) that Tramp has
already spawned for the connection:
- tramp-sh.el:5489-5514 unquoted string-join of login args
- tramp-sh.el:5406-5413 the local shell the string is sent to
tramp-user-regexp (tramp.el:1073) is
(+ (not (any "/:|[]" blank))), which admits $ ( ) ; ` ' " \ and more.
tramp-dissect-file-name performs no sanitization.
Consequence: merely stat-ing a file name such as
/ssh:$(cd;touch$IFS'pwned')@127.0.0.1:/x
e.g. via file-exists-p, executes the $(...) payload in the local
shell during connection setup, before and regardless of any actual
ssh connection or server. The excluded characters are easily worked
around within the allowed charset: $IFS substitutes for blanks,
$(printf$IFS'\057') composes the excluded /, and $(cd;...) sidesteps
absolute paths entirely, so arbitrary commands are expressible.
2. Tramp: file name dispatch regexp is line-anchored, not
string-anchored (tramp.el, CRITICAL enabler)
tramp-build-prefix-regexp (tramp.el:1025-1027) anchors with rx "bol";
the docstring even says the result "Should always start with ^". The
derived tramp-file-name-regexp is what Tramp registers in
file-name-handler-alist once loaded (replacing the string-anchored
tramp-initial-file-name-regexp autoload entry, tramp.el:1284-1288).
Consequence: after Tramp is loaded, any file name merely CONTAINING
newline + "/ssh:..." is dispatched to Tramp handlers, and
tramp-dissect-file-name parses the embedded line, feeding defect 1.
File names cannot contain "/", but two on-disk carriers exist:
a. symlink targets (arbitrary bytes except NUL, committable to
git);
b. a directory whose name ends in a newline, containing a file
named "ssh:...": the path component boundary supplies the "/",
so any code that composes the path (e.g.
directory-files-recursively) produces a string containing
"\n/ssh:...". Verified: recursively scanning such a tree
executes the payload with Tramp loaded.
Additionally, an ABSOLUTE symlink target "/ssh:$(...)@host:/x"
matches even the string-anchored autoload regexp, so no Tramp preload
is required for carrier (a).
--8<---------------cut here---------------end--------------->8---
--
Sean Whitton
Attachment:
tramp-fix.diff
Description:
Current thread:
- Emacs zero-click local command execution via TRAMP Sean Whitton (Aug 21)
- Re: Emacs zero-click local command execution via TRAMP nightmare . yeah27 (Aug 21)
- Re: Re: Emacs zero-click local command execution via TRAMP Sam James (Aug 23)
- Re: Emacs zero-click local command execution via TRAMP Sean Whitton (Aug 26)
- Re: Emacs zero-click local command execution via TRAMP nightmare . yeah27 (Aug 21)
