Skip to content

Commit a0da624

Browse files
Potential fix for code scanning alert no. 53: Client-side cross-site scripting (#579)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent d09515e commit a0da624

1 file changed

Lines changed: 59 additions & 1 deletion

File tree

  • vscode-extension/src/webview/usage

vscode-extension/src/webview/usage/main.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,64 @@ function setupTabs(): void {
436436
});
437437
}
438438

439+
function toSafeNumber(value: unknown): number {
440+
const n = Number(value);
441+
return Number.isFinite(n) && n >= 0 ? n : 0;
442+
}
443+
444+
function toSafeHttpUrl(value: unknown): string {
445+
const raw = typeof value === 'string' ? value.trim() : '';
446+
try {
447+
const parsed = new URL(raw);
448+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
449+
return parsed.toString();
450+
}
451+
} catch {
452+
// Ignore invalid URL and fall back to placeholder.
453+
}
454+
return '#';
455+
}
456+
457+
function sanitizeRepoPrStatsData(input: unknown): RepoPrStatsResult {
458+
const src = (input && typeof input === 'object') ? (input as Record<string, unknown>) : {};
459+
const repos = Array.isArray(src.repos) ? src.repos : [];
460+
return {
461+
authenticated: Boolean(src.authenticated),
462+
since: typeof src.since === 'string' || typeof src.since === 'number' ? src.since : Date.now(),
463+
repos: repos.map((repo) => {
464+
const r = (repo && typeof repo === 'object') ? (repo as Record<string, unknown>) : {};
465+
const aiDetails = Array.isArray(r.aiDetails) ? r.aiDetails : [];
466+
return {
467+
repoUrl: toSafeHttpUrl(r.repoUrl),
468+
owner: escapeHtml(typeof r.owner === 'string' ? r.owner : ''),
469+
repo: escapeHtml(typeof r.repo === 'string' ? r.repo : ''),
470+
error: typeof r.error === 'string' ? escapeHtml(r.error) : '',
471+
totalPrs: toSafeNumber(r.totalPrs),
472+
aiAuthoredPrs: toSafeNumber(r.aiAuthoredPrs),
473+
aiReviewRequestedPrs: toSafeNumber(r.aiReviewRequestedPrs),
474+
aiDetails: aiDetails.map((d) => {
475+
const detail = (d && typeof d === 'object') ? (d as Record<string, unknown>) : {};
476+
const validAiTypes = ['copilot', 'claude', 'openai', 'other-ai'] as const;
477+
const validRoles = ['author', 'reviewer-requested'] as const;
478+
const aiType = validAiTypes.includes(detail.aiType as typeof validAiTypes[number])
479+
? detail.aiType as typeof validAiTypes[number]
480+
: 'other-ai';
481+
const role = validRoles.includes(detail.role as typeof validRoles[number])
482+
? detail.role as typeof validRoles[number]
483+
: 'author';
484+
return {
485+
number: toSafeNumber(detail.number),
486+
title: escapeHtml(typeof detail.title === 'string' ? detail.title : ''),
487+
url: toSafeHttpUrl(detail.url),
488+
aiType,
489+
role,
490+
};
491+
}),
492+
};
493+
}),
494+
} as RepoPrStatsResult;
495+
}
496+
439497
function renderReposPrContent(data: RepoPrStatsResult): string {
440498
const sinceDate = escapeHtml(new Date(data.since).toLocaleDateString());
441499
if (!data.authenticated) {
@@ -1356,7 +1414,7 @@ window.addEventListener('message', (event) => {
13561414
break;
13571415
}
13581416
case 'repoPrStatsLoaded': {
1359-
repoPrStatsData = message.data as RepoPrStatsResult;
1417+
repoPrStatsData = sanitizeRepoPrStatsData(message.data);
13601418
// Reset the loaded flag when not authenticated so re-authenticating and clicking the tab
13611419
// again triggers a fresh fetch instead of showing the stale "not authenticated" placeholder.
13621420
if (!repoPrStatsData.authenticated) {

0 commit comments

Comments
 (0)