Submit #1007305: https://github.com/ffxiv-teamcraft ffxiv-teamcraft 11.4.31 Local Sensitive Logic Leading to webpage jumpinginfo

Titlehttps://github.com/ffxiv-teamcraft ffxiv-teamcraft 11.4.31 Local Sensitive Logic Leading to webpage jumping
Description| Item | Details | |---|---| | Affected software | FFXIV Teamcraft (web and desktop apps share the front-end code; the desktop app is an Electron application) | | Affected components | ① DB page comment feature (`apps/client/src/app/pages/db/db-comments/`); ② renderer-process IPC bridge (`apps/electron/src/preload.ts`) and main-process IPC listeners (`apps/electron/src/ipc/ipc-listeners-manager.ts`) | | Affected version | FFXIV Teamcraft v11.4.31 (latest release at the time of reporting) | | Attack prerequisite | The attacker is a logged-in user (submits the malicious comment); the victims are any users who view the same DB page (no other interaction required) | | Severity | High | | Vulnerability type | Stored cross-site scripting CWE-79 + sensitive information exposure CWE-200 / improper privilege control CWE-284 (chained) | --- ## 1. Vulnerability Description This vulnerability is a complete attack chain built from two reliably connected defects: **Stage 1: persistent stored XSS in database comments.** The entire path from submission, storage, to display of comment content has no HTML escaping or sanitization: the submit side writes the textarea content into the comment record as-is; the display side renders it into an Angular `[innerHTML]` binding; and the link-handling pipe in between concatenates the raw string and then calls `bypassSecurityTrustHtml` to trust the whole result (that call by definition "skips Angular's built-in sanitization", and the pipe contains no escaping or sanitization call of any kind). Any logged-in user can persist arbitrary HTML/scripts in a comment, and every user who subsequently views that page will have the content executed in their browser (web) or renderer (desktop app). **Stage 2: the renderer-process IPC bridge exposes sensitive capabilities.** The desktop app exposes a channel-allowlist IPC bridge to the renderer via `contextBridge.exposeInMainWorld('ipc', ...)`; the allowlist only checks "is the channel name allowed" and does not distinguish the caller's context — an attacker who gained script execution through stage 1 can directly invoke main-process capabilities: 1. **Read the system clipboard** (`clipboard:get` → main-process `clipboard.readText()`, system-wide) — can be polled periodically to steal passwords, 2FA codes, and tokens that the user copies in daily use; 2. **Open arbitrary external links** (`open-link`, validates only the protocol, not the domain) — a phishing springboard; 3. **Forge system notifications** (`notification`, with the official Teamcraft icon) — e.g., a fake "new version available" prompt to induce downloads; 4. **Persistently hijack the proxy/PAC settings** (`proxy-rule`/`proxy-pac`, written to the local config and **still effective after restart**) — all subsequent application requests (Firebase, ffxivteamcraft.com, including logged-in sessions) can be routed to the attacker's proxy; 5. **Open arbitrary routes in always-on-top overlay / child windows** (`overlay`/`child:new`) — UI spoofing inside the trusted app; 6. **Read local data**: application state (lists/layouts), the local inventory file, and the item-order data in the FFXIV game save directory (including every character ContentID). **Combined impact**: any desktop user who simply views a malicious comment can have their clipboard silently stolen (passwords/2FA), their application traffic persistently hijacked, and be phished via official-icon notifications — all inside the trusted application, with no user interaction beyond opening the page. On the web, the impact is that of a classic stored XSS (session abuse / in-app operations / injected phishing content). --- ## 2. Root Cause Analysis (Key Source Code) ### 2.1 Stage 1 · Render side: the `[innerHTML]` binding `apps/client/src/app/pages/db/db-comments/db-comments/db-comments.component.html` (lines 48-49): ```html <p class="comment-content" [innerHTML]="comment.message | commentLinks:translate.currentLang:comment | async"></p> ``` The comment content is piped through `commentLinks` and bound to `[innerHTML]` — whether this is safe depends entirely on whether the pipe escapes. ### 2.2 Stage 1 · Pipe side: concatenation, then blanket trust, with no escaping `apps/client/src/app/pages/db/db-comments/comment-links.pipe.ts` (lines 116-120, excerpt): ```ts // ... regex replacement over the raw string, concatenating anchor HTML ... return this.sanitizer.bypassSecurityTrustHtml(value); ``` The pipe performs plain string concatenation over the user-controlled raw string and then calls `bypassSecurityTrustHtml` to trust the entire result. The file contains no `escapeHtml`, `DOMPurify`, or `sanitizer.sanitize` — no escaping or sanitization call of any kind. ### 2.3 Stage 1 · Submit side: arbitrary content stored as-is `apps/client/src/app/pages/db/db-comments/db-comments/db-comments.component.ts` (lines 186-199, excerpt): ```ts const comment = ...; comment.message = this.newCommentContent; // ← textarea raw text, unfiltered this.newCommentContent = ''; // followed by commentsService.add(comment) writing it to the database ``` The submission template (`db-comments.component.html:133-145`) is a plain textarea that accepts arbitrary text. ### 2.4 Stage 2 · The IPC bridge is open to any renderer script `apps/electron/src/preload.ts` (lines 158-180): ```ts contextBridge.exposeInMainWorld('ipc', { init: () => { ipcRenderer.setMaxListeners(0); }, removeAllListeners: (channel: string) => { ipcRenderer.removeAllListeners(channel); }, send: (channel: string, ...data: any[]) => { if (checkChannel(channel)) { ipcRenderer.send(channel, ...data); } }, on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => { if (checkChannel(channel)) { ipcRenderer.on(channel, listener); } }, ... }); ``` The allowlist (`preload.ts:5-137`) includes dozens of channels such as `clipboard`, `clipboard:get`, `open-link`, `notification`, `proxy-rule`, `proxy-pac`, `overlay`, `child:new`, `app-state:get`, `inventory:get`, and `dat:all-odr`; `checkChannel` only performs a name match and never checks the caller's origin or context. ### 2.5 Stage 2 · System clipboard read (no origin / gesture check) `apps/electron/src/ipc/ipc-listeners-manager.ts` (lines 168-170): ```ts ipcMain.on('clipboard:get', (event) => { event.sender.send('clipboard', clipboard.readText()); // ← system-wide clipboard, callable by any script }); ``` The legitimate business use is "recipe finder import from clipboard" (`apps/client/src/app/pages/recipe-finder/recipe-finder/recipe-finder.component.ts:296-305`), but the channel is open to any renderer-process script, with no user-gesture restriction. ### 2.6 Stage 2 · Opening arbitrary links and forging system notifications `ipc-listeners-manager.ts` (lines 390-415): ```ts ipcMain.on('open-link', (event, url) => { if (!['https:', 'http:'].includes(new URL(url).protocol)) return; // ← validates only the protocol, not the domain shell.openExternal(url); }); ipcMain.on('notification', (event, config) => { const iconPath = join(Constants.BASE_APP_PATH, 'assets/app-icon.png'); if (process.platform === 'win32' && this.trayMenu.tray) { config.icon = nativeImage.createFromPath(iconPath); // ← with the official icon config.silent = true; this.trayMenu.tray.displayBalloon(config); // ← Windows tray balloon, content fully caller-controlled } else { ... new Notification({...}).show(); } }); ```
Source⚠️ https://drive.google.com/drive/folders/1pxitXRLnnnl9GPnyN83ILAgWHWve3ezX?usp=sharing
User
 PoriaMs (UID 101188)
Submission09/30/2026 09:36 (1 month ago)
Moderation10/25/2026 15:48 (25 days later)
StatusAccepted
VulDB entry426278 [ffxiv-teamcraft FFXIV Teamcraft up to 11.4.31 Renderer-Process IPC Bridge preload.ts clipboard.readText cross site scripting]
Points20

Do you know our Splunk app?

Download it now for free!