Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
} from '@microsoft/kiota';
import * as vscode from 'vscode';

import { escapeHtml } from '../utilities/html';

export class DependenciesViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
public constructor(
Expand All @@ -14,8 +16,7 @@ export class DependenciesViewProvider implements vscode.WebviewViewProvider {
public resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext<unknown>, token: vscode.CancellationToken): void | Thenable<void> {
this._view = webviewView;
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
enableScripts: false,

localResourceRoots: [
this._extensionUri
Expand Down Expand Up @@ -50,25 +51,26 @@ 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 ? `<h2>${installationCommands}</h2>
<pre>${dependenciesList.map(dep => this._languageInformation!.DependencyInstallCommand.replace(/\{0\}/g, dep.Name).replace(/\{1\}/g, dep.Version)).join('\n')}</pre>`
const installationBlock = this._languageInformation?.DependencyInstallCommand ? `<h2>${escapeHtml(installationCommands)}</h2>
<pre>${dependenciesList.map(dep => escapeHtml(this._languageInformation!.DependencyInstallCommand.replace(/\{0\}/g, dep.Name).replace(/\{1\}/g, dep.Version))).join('\n')}</pre>`
: '';

return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; img-src ${webview.cspSource}; script-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleResetUri}" rel="stylesheet">
<link href="${styleVSCodeUri}" rel="stylesheet">
<title>${title}</title>
<title>${escapeHtml(title)}</title>
</head>
<body>
<h1>${this._language !== undefined ? generationLanguageToString(this._language) : noLanguageSelected}</h1>
<h2>${dependencies}</h2>
<h1>${this._language !== undefined ? escapeHtml(generationLanguageToString(this._language)) : escapeHtml(noLanguageSelected)}</h1>
<h2>${escapeHtml(dependencies)}</h2>
<table>
<tr><th>${name}</th><th>${version}</th><th>${type}</th></tr>
${dependenciesList.map(dep => `<tr><td>${dep.Name}</td><td>${dep.Version}</td><td>${dependencyTypeToString(dep.DependencyType)}</td></tr>`).join('')}
<tr><th>${escapeHtml(name)}</th><th>${escapeHtml(version)}</th><th>${escapeHtml(type)}</th></tr>
${dependenciesList.map(dep => `<tr><td>${escapeHtml(dep.Name)}</td><td>${escapeHtml(dep.Version)}</td><td>${escapeHtml(dependencyTypeToString(dep.DependencyType))}</td></tr>`).join('')}
</table>
${installationBlock}
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '<script>alert(1)</script>',
// eslint-disable-next-line @typescript-eslint/naming-convention
Version: '"><img src=x onerror=alert(2)>',
// 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<unknown>, {} as vscode.CancellationToken);

const html = webview.html;

assert.ok(!html.includes('<script>alert(1)</script>'), 'raw script tag must not appear unescaped');
assert.ok(!html.includes('<img src=x onerror=alert(2)>'), 'raw injected img tag must not appear unescaped');
assert.ok(html.includes('&lt;script&gt;'), 'dependency name should be HTML-escaped');
assert.ok(html.includes('&quot;&gt;&lt;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<unknown>, {} 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 'none'"), 'expected script-src to be disabled entirely');
});
});
12 changes: 12 additions & 0 deletions vscode/packages/microsoft-kiota/src/utilities/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
Loading