From e7b066b600297c66b014515671c8d0214b64abef Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 23:22:03 +0200 Subject: [PATCH 1/2] Potential fix for code scanning alert no. 53: Client-side cross-site scripting Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 49 +++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 11aeae8e..069c19d1 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -436,6 +436,53 @@ function setupTabs(): void { }); } +function toSafeNumber(value: unknown): number { + const n = Number(value); + return Number.isFinite(n) && n >= 0 ? n : 0; +} + +function toSafeHttpUrl(value: unknown): string { + const raw = typeof value === 'string' ? value.trim() : ''; + try { + const parsed = new URL(raw); + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + return parsed.toString(); + } + } catch { + // Ignore invalid URL and fall back to placeholder. + } + return '#'; +} + +function sanitizeRepoPrStatsData(input: unknown): RepoPrStatsResult { + const src = (input && typeof input === 'object') ? (input as Record) : {}; + const repos = Array.isArray(src.repos) ? src.repos : []; + return { + authenticated: Boolean(src.authenticated), + since: typeof src.since === 'string' || typeof src.since === 'number' ? src.since : Date.now(), + repos: repos.map((repo) => { + const r = (repo && typeof repo === 'object') ? (repo as Record) : {}; + const aiDetails = Array.isArray(r.aiDetails) ? r.aiDetails : []; + return { + repoUrl: toSafeHttpUrl(r.repoUrl), + owner: escapeHtml(typeof r.owner === 'string' ? r.owner : ''), + repo: escapeHtml(typeof r.repo === 'string' ? r.repo : ''), + error: typeof r.error === 'string' ? escapeHtml(r.error) : '', + totalPrs: toSafeNumber(r.totalPrs), + aiAuthoredPrs: toSafeNumber(r.aiAuthoredPrs), + aiReviewRequestedPrs: toSafeNumber(r.aiReviewRequestedPrs), + aiDetails: aiDetails.map((d) => { + const detail = (d && typeof d === 'object') ? (d as Record) : {}; + return { + label: escapeHtml(typeof detail.label === 'string' ? detail.label : ''), + count: toSafeNumber(detail.count), + }; + }), + }; + }), + } as RepoPrStatsResult; +} + function renderReposPrContent(data: RepoPrStatsResult): string { const sinceDate = escapeHtml(new Date(data.since).toLocaleDateString()); if (!data.authenticated) { @@ -1356,7 +1403,7 @@ window.addEventListener('message', (event) => { break; } case 'repoPrStatsLoaded': { - repoPrStatsData = message.data as RepoPrStatsResult; + repoPrStatsData = sanitizeRepoPrStatsData(message.data); // Reset the loaded flag when not authenticated so re-authenticating and clicking the tab // again triggers a fresh fetch instead of showing the stale "not authenticated" placeholder. if (!repoPrStatsData.authenticated) { From 4e13c9afad087b01923c2a3a069a03371acfcd65 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 23:32:21 +0200 Subject: [PATCH 2/2] fix: sanitize RepoPrDetail fields correctly in sanitizeRepoPrStatsData The CodeQL autofix introduced sanitizeRepoPrStatsData but mapped aiDetails to {label, count} instead of the required RepoPrDetail shape {number, title, url, aiType, role}, causing a TS2352 type error and CI build failure. Replace the incorrect mapping with proper sanitization: - number: coerced to safe non-negative number - title: HTML-escaped string - url: validated to http/https via toSafeHttpUrl - aiType: constrained to allowed union values, fallback 'other-ai' - role: constrained to allowed union values, fallback 'author' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 069c19d1..587a5bbb 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -473,9 +473,20 @@ function sanitizeRepoPrStatsData(input: unknown): RepoPrStatsResult { aiReviewRequestedPrs: toSafeNumber(r.aiReviewRequestedPrs), aiDetails: aiDetails.map((d) => { const detail = (d && typeof d === 'object') ? (d as Record) : {}; + const validAiTypes = ['copilot', 'claude', 'openai', 'other-ai'] as const; + const validRoles = ['author', 'reviewer-requested'] as const; + const aiType = validAiTypes.includes(detail.aiType as typeof validAiTypes[number]) + ? detail.aiType as typeof validAiTypes[number] + : 'other-ai'; + const role = validRoles.includes(detail.role as typeof validRoles[number]) + ? detail.role as typeof validRoles[number] + : 'author'; return { - label: escapeHtml(typeof detail.label === 'string' ? detail.label : ''), - count: toSafeNumber(detail.count), + number: toSafeNumber(detail.number), + title: escapeHtml(typeof detail.title === 'string' ? detail.title : ''), + url: toSafeHttpUrl(detail.url), + aiType, + role, }; }), };