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).
This commit is contained in:
nopoz
2026-06-19 02:03:44 -07:00
committed by GitHub
parent a226c94df7
commit 076e8c93c9
3 changed files with 6 additions and 6 deletions
+2 -2
View File
@@ -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));`;
+2 -2
View File
@@ -635,8 +635,8 @@ export function applyModelColor(roleEl, modelName) {
popup.className = 'ctx-popup';
let html = '<div style="font-weight:600;margin-bottom:6px;color:var(--fg);display:flex;align-items:center;gap:6px;">';
if (logoHtml) html += '<span class="role-provider-logo" style="opacity:0.7">' + logoHtml + '</span>';
html += short + '</div>';
html += '<div><span class="ctx-label">Model</span> ' + modelName.split('/').pop() + '</div>';
html += uiModule.esc(short) + '</div>';
html += '<div><span class="ctx-label">Model</span> ' + uiModule.esc(modelName.split('/').pop()) + '</div>';
// 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)
+2 -2
View File
@@ -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 = `<span class="doc-tab-lang">${lic}</span>`;
html += `<div class="doc-tab${isActive ? ' active' : ''}" draggable="true" data-doc-id="${id}" title="${title}">
${verChip}${langChip}<span class="doc-tab-title">${shortTitle}</span>
html += `<div class="doc-tab${isActive ? ' active' : ''}" draggable="true" data-doc-id="${id}" title="${_esc(title)}">
${verChip}${langChip}<span class="doc-tab-title">${_esc(shortTitle)}</span>
<button class="doc-tab-close" data-doc-id="${id}" title="Unlink from chat (kept in the Library)">&times;</button>
</div>`;
}