From 076e8c93c92f051013ca46bd3989e821e1088445 Mon Sep 17 00:00:00 2001 From: nopoz Date: Fri, 19 Jun 2026 02:03:44 -0700 Subject: [PATCH] fix(ui): escape model name in model-info popup (DOM-XSS) + two latent sinks (#4605) chatRenderer.js built the model-info popup HTML by concatenating the model name (from the LLM response's model/answered_by field) into popup.innerHTML without escaping, so a model advertised as an HTML/script payload executed when the user clicked the role label. Wrap both insertions with the uiModule.esc() helper the same function already uses. Also apply existing escape helpers at two latent sinks flagged by CodeQL, fed only by self-authored/server values today: document-tab title via _esc(), and the calendar event background URL (escape the double quote that would otherwise break out of the style="..." attribute). --- static/js/calendar.js | 4 ++-- static/js/chatRenderer.js | 4 ++-- static/js/document.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/static/js/calendar.js b/static/js/calendar.js index 4c5c38564..2b14d024a 100644 --- a/static/js/calendar.js +++ b/static/js/calendar.js @@ -413,7 +413,7 @@ function _calEventFg(ev) { // Returns '' for normal solid-color events. function _calItemBgStyle(ev) { if (!_isCalBgImage(ev.color)) return ''; - const url = _calBgImageUrl(ev.color).replace(/'/g, "\\'"); + const url = _calBgImageUrl(ev.color).replace(/'/g, "\\'").replace(/"/g, "%22"); return `background-image: linear-gradient(color-mix(in srgb, var(--bg) 70%, transparent), color-mix(in srgb, var(--bg) 70%, transparent)), url('${url}'); background-size: cover; background-position: center;`; } @@ -1260,7 +1260,7 @@ async function _renderWeek() { // events keep the original tinted treatment. let bgDecl; if (_isCalBgImage(ev.color)) { - const _url = _calBgImageUrl(ev.color).replace(/'/g, "\\'"); + const _url = _calBgImageUrl(ev.color).replace(/'/g, "\\'").replace(/"/g, "%22"); bgDecl = `background-image: linear-gradient(color-mix(in srgb, var(--bg) 55%, transparent), color-mix(in srgb, var(--bg) 55%, transparent)), url('${_url}'); background-size: cover; background-position: center;`; } else { bgDecl = `background:color-mix(in srgb, ${_calColor(ev)} 18%, var(--bg));`; diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js index ce98be4b9..253fa5724 100644 --- a/static/js/chatRenderer.js +++ b/static/js/chatRenderer.js @@ -635,8 +635,8 @@ export function applyModelColor(roleEl, modelName) { popup.className = 'ctx-popup'; let html = '
'; if (logoHtml) html += ''; - html += short + '
'; - html += '
Model ' + modelName.split('/').pop() + '
'; + html += uiModule.esc(short) + ''; + html += '
Model ' + uiModule.esc(modelName.split('/').pop()) + '
'; // Provider = the serving endpoint, distinct from the model vendor/logo // (e.g. the same model via OpenRouter vs Copilot vs Anthropic direct). const _epUrl = (window.sessionModule && window.sessionModule.getCurrentEndpointUrl) diff --git a/static/js/document.js b/static/js/document.js index 20d77788a..b314c0589 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -284,8 +284,8 @@ import * as Modals from './modalManager.js'; ? langIcon(doc.language, 12, { style: 'opacity:0.65;flex-shrink:0;color:currentColor;margin-right:4px;' }) : ''; const langChip = `${lic}`; - html += `
- ${verChip}${langChip}${shortTitle} + html += `
+ ${verChip}${langChip}${_esc(shortTitle)}
`; }