From f0ab46d5e567e9d17d29d6c271f026d9b97053ec Mon Sep 17 00:00:00 2001 From: "Peter Ombwa (from Dev Box)" Date: Wed, 5 Aug 2026 16:08:04 -0700 Subject: [PATCH 1/3] fix(vscode): sanitize and lock down Dependencies webview to prevent XSS --- .../src/providers/dependenciesViewProvider.ts | 18 ++-- .../dependenciesViewProvider.test.ts | 85 +++++++++++++++++++ .../microsoft-kiota/src/utilities/html.ts | 25 ++++++ 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts create mode 100644 vscode/packages/microsoft-kiota/src/utilities/html.ts diff --git a/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts b/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts index 5eb56a6752..401f08222e 100644 --- a/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts +++ b/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts @@ -4,6 +4,8 @@ import { } from '@microsoft/kiota'; import * as vscode from 'vscode'; +import { escapeHtml, getNonce } from '../utilities/html'; + export class DependenciesViewProvider implements vscode.WebviewViewProvider { private _view?: vscode.WebviewView; public constructor( @@ -50,25 +52,27 @@ export class DependenciesViewProvider implements vscode.WebviewViewProvider { if (dependenciesList.filter(dep => dep.DependencyType === DependencyType.bundle).length > 0) { dependenciesList = dependenciesList.filter(dep => dep.DependencyType === DependencyType.bundle || dep.DependencyType === DependencyType.additional || dep.DependencyType === DependencyType.authentication); } - const installationBlock = this._languageInformation?.DependencyInstallCommand ? `

${installationCommands}

-
${dependenciesList.map(dep => this._languageInformation!.DependencyInstallCommand.replace(/\{0\}/g, dep.Name).replace(/\{1\}/g, dep.Version)).join('\n')}
` + const nonce = getNonce(); + const installationBlock = this._languageInformation?.DependencyInstallCommand ? `

${escapeHtml(installationCommands)}

+
${dependenciesList.map(dep => escapeHtml(this._languageInformation!.DependencyInstallCommand.replace(/\{0\}/g, dep.Name).replace(/\{1\}/g, dep.Version))).join('\n')}
` : ''; return ` + - ${title} + ${escapeHtml(title)} -

${this._language !== undefined ? generationLanguageToString(this._language) : noLanguageSelected}

-

${dependencies}

+

${this._language !== undefined ? escapeHtml(generationLanguageToString(this._language)) : escapeHtml(noLanguageSelected)}

+

${escapeHtml(dependencies)}

- - ${dependenciesList.map(dep => ``).join('')} + + ${dependenciesList.map(dep => ``).join('')}
${name}${version}${type}
${dep.Name}${dep.Version}${dependencyTypeToString(dep.DependencyType)}
${escapeHtml(name)}${escapeHtml(version)}${escapeHtml(type)}
${escapeHtml(dep.Name)}${escapeHtml(dep.Version)}${escapeHtml(dependencyTypeToString(dep.DependencyType))}
${installationBlock} diff --git a/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts b/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts new file mode 100644 index 0000000000..e2e0fd2e0c --- /dev/null +++ b/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts @@ -0,0 +1,85 @@ +import { DependencyType, KiotaGenerationLanguage, LanguageInformation, MaturityLevel } from "@microsoft/kiota"; +import assert from "assert"; +import * as vscode from 'vscode'; + +import { DependenciesViewProvider } from "../../../providers/dependenciesViewProvider"; + +function createFakeWebview(): vscode.Webview { + return { + cspSource: "vscode-webview://fake-csp-source", + asWebviewUri: (uri: vscode.Uri) => uri, + html: "", + options: {}, + onDidReceiveMessage: (() => ({ dispose: () => { } })) as any, + postMessage: (async () => true) as any, + } as unknown as vscode.Webview; +} + +suite('DependenciesViewProvider Test Suite', () => { + const extensionUri = vscode.Uri.parse('file:///fake-extension'); + + test('escapes malicious dependency name/version to prevent XSS', () => { + const maliciousLanguageInformation: LanguageInformation = { + // eslint-disable-next-line @typescript-eslint/naming-convention + MaturityLevel: MaturityLevel.stable, + // eslint-disable-next-line @typescript-eslint/naming-convention + ClientNamespaceName: 'ns', + // eslint-disable-next-line @typescript-eslint/naming-convention + ClientClassName: 'cls', + // eslint-disable-next-line @typescript-eslint/naming-convention + StructuredMimeTypes: [], + // eslint-disable-next-line @typescript-eslint/naming-convention + DependencyInstallCommand: 'install {0}@{1}', + // eslint-disable-next-line @typescript-eslint/naming-convention + Dependencies: [ + { + // eslint-disable-next-line @typescript-eslint/naming-convention + Name: '', + // eslint-disable-next-line @typescript-eslint/naming-convention + Version: '">', + // eslint-disable-next-line @typescript-eslint/naming-convention + DependencyType: DependencyType.bundle + } + ] + }; + + const provider = new DependenciesViewProvider(extensionUri, maliciousLanguageInformation, KiotaGenerationLanguage.CSharp); + const webview = createFakeWebview(); + // resolveWebviewView sets webview.html via the private _getHtmlForWebview method + (provider as any).resolveWebviewView({ webview, show: () => { } } as unknown as vscode.WebviewView, {} as vscode.WebviewViewResolveContext, {} as vscode.CancellationToken); + + const html = webview.html; + + assert.ok(!html.includes(''), 'raw script tag must not appear unescaped'); + assert.ok(!html.includes(''), 'raw injected img tag must not appear unescaped'); + assert.ok(html.includes('<script>'), 'dependency name should be HTML-escaped'); + assert.ok(html.includes('"><img'), 'dependency version should be HTML-escaped'); + }); + + test('includes a Content-Security-Policy meta tag', () => { + const languageInformation: LanguageInformation = { + // eslint-disable-next-line @typescript-eslint/naming-convention + MaturityLevel: MaturityLevel.stable, + // eslint-disable-next-line @typescript-eslint/naming-convention + ClientNamespaceName: 'ns', + // eslint-disable-next-line @typescript-eslint/naming-convention + ClientClassName: 'cls', + // eslint-disable-next-line @typescript-eslint/naming-convention + StructuredMimeTypes: [], + // eslint-disable-next-line @typescript-eslint/naming-convention + DependencyInstallCommand: 'install {0}@{1}', + // eslint-disable-next-line @typescript-eslint/naming-convention + Dependencies: [ + // eslint-disable-next-line @typescript-eslint/naming-convention + { Name: 'SomeDep', Version: '1.0.0', DependencyType: DependencyType.bundle } + ] + }; + + const provider = new DependenciesViewProvider(extensionUri, languageInformation, KiotaGenerationLanguage.CSharp); + const webview = createFakeWebview(); + (provider as any).resolveWebviewView({ webview, show: () => { } } as unknown as vscode.WebviewView, {} as vscode.WebviewViewResolveContext, {} as vscode.CancellationToken); + + assert.ok(webview.html.includes('Content-Security-Policy'), 'expected a CSP meta tag in the webview HTML'); + assert.ok(webview.html.includes("script-src 'nonce-"), 'expected script-src to be restricted to a nonce'); + }); +}); diff --git a/vscode/packages/microsoft-kiota/src/utilities/html.ts b/vscode/packages/microsoft-kiota/src/utilities/html.ts new file mode 100644 index 0000000000..08319e645b --- /dev/null +++ b/vscode/packages/microsoft-kiota/src/utilities/html.ts @@ -0,0 +1,25 @@ +/** + * Escapes HTML-significant characters in a string so it can be safely + * interpolated into webview HTML without introducing script/markup injection. + */ +export function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Generates a cryptographically random nonce suitable for use in a + * webview Content-Security-Policy `script-src` directive. + */ +export function getNonce(): string { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} From 915847a33b041daef3a178aa6011c5ade08fe06b Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Wed, 5 Aug 2026 16:21:23 -0700 Subject: [PATCH 2/3] chore: use Node's randombytes for nonce generation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- vscode/packages/microsoft-kiota/src/utilities/html.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/vscode/packages/microsoft-kiota/src/utilities/html.ts b/vscode/packages/microsoft-kiota/src/utilities/html.ts index 08319e645b..45a2a1125d 100644 --- a/vscode/packages/microsoft-kiota/src/utilities/html.ts +++ b/vscode/packages/microsoft-kiota/src/utilities/html.ts @@ -1,3 +1,5 @@ +import { randomBytes } from 'node:crypto'; + /** * Escapes HTML-significant characters in a string so it can be safely * interpolated into webview HTML without introducing script/markup injection. @@ -16,10 +18,6 @@ export function escapeHtml(value: string): string { * webview Content-Security-Policy `script-src` directive. */ export function getNonce(): string { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; + // 16 bytes => 128-bit nonce + return randomBytes(16).toString('hex'); } From a62d951e72d073d3fec6b3f7f5fd73bccbd734ec Mon Sep 17 00:00:00 2001 From: "Peter Ombwa (from Dev Box)" Date: Wed, 5 Aug 2026 16:50:02 -0700 Subject: [PATCH 3/3] chore: remove getNonce --- .../src/providers/dependenciesViewProvider.ts | 8 +++----- .../suite/providers/dependenciesViewProvider.test.ts | 2 +- vscode/packages/microsoft-kiota/src/utilities/html.ts | 11 ----------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts b/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts index 401f08222e..3e505cc16a 100644 --- a/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts +++ b/vscode/packages/microsoft-kiota/src/providers/dependenciesViewProvider.ts @@ -4,7 +4,7 @@ import { } from '@microsoft/kiota'; import * as vscode from 'vscode'; -import { escapeHtml, getNonce } from '../utilities/html'; +import { escapeHtml } from '../utilities/html'; export class DependenciesViewProvider implements vscode.WebviewViewProvider { private _view?: vscode.WebviewView; @@ -16,8 +16,7 @@ export class DependenciesViewProvider implements vscode.WebviewViewProvider { public resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): void | Thenable { this._view = webviewView; webviewView.webview.options = { - // Allow scripts in the webview - enableScripts: true, + enableScripts: false, localResourceRoots: [ this._extensionUri @@ -52,7 +51,6 @@ export class DependenciesViewProvider implements vscode.WebviewViewProvider { if (dependenciesList.filter(dep => dep.DependencyType === DependencyType.bundle).length > 0) { dependenciesList = dependenciesList.filter(dep => dep.DependencyType === DependencyType.bundle || dep.DependencyType === DependencyType.additional || dep.DependencyType === DependencyType.authentication); } - const nonce = getNonce(); const installationBlock = this._languageInformation?.DependencyInstallCommand ? `

${escapeHtml(installationCommands)}

${dependenciesList.map(dep => escapeHtml(this._languageInformation!.DependencyInstallCommand.replace(/\{0\}/g, dep.Name).replace(/\{1\}/g, dep.Version))).join('\n')}
` : ''; @@ -61,7 +59,7 @@ export class DependenciesViewProvider implements vscode.WebviewViewProvider { - + diff --git a/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts b/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts index e2e0fd2e0c..d9077c4bd7 100644 --- a/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts +++ b/vscode/packages/microsoft-kiota/src/test/suite/providers/dependenciesViewProvider.test.ts @@ -80,6 +80,6 @@ suite('DependenciesViewProvider Test Suite', () => { (provider as any).resolveWebviewView({ webview, show: () => { } } as unknown as vscode.WebviewView, {} as vscode.WebviewViewResolveContext, {} as vscode.CancellationToken); assert.ok(webview.html.includes('Content-Security-Policy'), 'expected a CSP meta tag in the webview HTML'); - assert.ok(webview.html.includes("script-src 'nonce-"), 'expected script-src to be restricted to a nonce'); + assert.ok(webview.html.includes("script-src 'none'"), 'expected script-src to be disabled entirely'); }); }); diff --git a/vscode/packages/microsoft-kiota/src/utilities/html.ts b/vscode/packages/microsoft-kiota/src/utilities/html.ts index 45a2a1125d..58df3fa0f6 100644 --- a/vscode/packages/microsoft-kiota/src/utilities/html.ts +++ b/vscode/packages/microsoft-kiota/src/utilities/html.ts @@ -1,5 +1,3 @@ -import { randomBytes } from 'node:crypto'; - /** * Escapes HTML-significant characters in a string so it can be safely * interpolated into webview HTML without introducing script/markup injection. @@ -12,12 +10,3 @@ export function escapeHtml(value: string): string { .replace(/"/g, '"') .replace(/'/g, '''); } - -/** - * Generates a cryptographically random nonce suitable for use in a - * webview Content-Security-Policy `script-src` directive. - */ -export function getNonce(): string { - // 16 bytes => 128-bit nonce - return randomBytes(16).toString('hex'); -}