Skip to content

Commit

Permalink
Adds better webview security
Browse files Browse the repository at this point in the history
  • Loading branch information
eamodio committed Jul 9, 2021
1 parent afa3252 commit f77cf30
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 69 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds new _Open Previous Changes with Working File_ command to commit files in views — closes [#1529](https://github.com/eamodio/vscode-gitlens/issues/1529)
- Adopts new vscode `createStatusBarItem` API to allow for independent toggling — closes [#1543](https://github.com/eamodio/vscode-gitlens/issues/1543)

### Changed

- Dynamically generates hashes and nonces for webview script and style tags for better

### Fixed

- Fixes [#1432](https://github.com/eamodio/vscode-gitlens/issues/1432) - Unhandled Timeout Promise
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/apps/rebase/rebase.html
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<style nonce="Z2l0bGVucy1ib290c3RyYXA=">
<style nonce="#{cspNonce}">
@font-face {
font-family: 'codicon';
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/apps/settings/settings.html
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<style nonce="Z2l0bGVucy1ib290c3RyYXA=">
<style nonce="#{cspNonce}">
@font-face {
font-family: 'codicon';
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/apps/welcome/welcome.html
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<style nonce="Z2l0bGVucy1ib290c3RyYXA=">
<style nonce="#{cspNonce}">
@font-face {
font-family: 'codicon';
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');
Expand Down
39 changes: 28 additions & 11 deletions src/webviews/rebaseEditor.ts
@@ -1,4 +1,5 @@
'use strict';
import { randomBytes } from 'crypto';
import { TextDecoder } from 'util';
import {
CancellationToken,
Expand Down Expand Up @@ -476,18 +477,34 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', 'rebase.html');
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));

let html = content
.replace(/#{cspSource}/g, context.panel.webview.cspSource)
.replace(/#{root}/g, context.panel.webview.asWebviewUri(Container.context.extensionUri).toString());

const bootstrap = await this.parseState(context);

html = html.replace(
/#{endOfBody}/i,
`<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
bootstrap,
)};</script>`,
);
const cspSource = context.panel.webview.cspSource;
const cspNonce = randomBytes(16).toString('base64');
const root = context.panel.webview.asWebviewUri(Container.context.extensionUri).toString();

const html = content
.replace(/#{(head|body|endOfBody)}/i, (_substring, token) => {
switch (token) {
case 'endOfBody':
return `<script type="text/javascript" nonce="#{cspNonce}">window.bootstrap = ${JSON.stringify(
bootstrap,
)};</script>`;
default:
return '';
}
})
.replace(/#{(cspSource|cspNonce|root)}/g, (substring, token) => {
switch (token) {
case 'cspSource':
return cspSource;
case 'cspNonce':
return cspNonce;
case 'root':
return root;
default:
return '';
}
});

return html;
}
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/settingsWebview.ts
Expand Up @@ -99,7 +99,7 @@ export class SettingsWebview extends WebviewBase {
scope: 'user',
scopes: scopes,
};
return `<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
return `<script type="text/javascript" nonce="#{cspNonce}">window.bootstrap = ${JSON.stringify(
bootstrap,
)};</script>`;
}
Expand Down
51 changes: 36 additions & 15 deletions src/webviews/webviewBase.ts
@@ -1,4 +1,5 @@
'use strict';
import { randomBytes } from 'crypto';
import { TextDecoder } from 'util';
import {
commands,
Expand Down Expand Up @@ -325,21 +326,41 @@ export abstract class WebviewBase implements Disposable {
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', this.filename);
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));

let html = content
.replace(/#{cspSource}/g, webview.cspSource)
.replace(/#{root}/g, webview.asWebviewUri(Container.context.extensionUri).toString());

if (this.renderHead != null) {
html = html.replace(/#{head}/i, await this.renderHead());
}

if (this.renderBody != null) {
html = html.replace(/#{body}/i, await this.renderBody());
}

if (this.renderEndOfBody != null) {
html = html.replace(/#{endOfBody}/i, await this.renderEndOfBody());
}
const [head, body, endOfBody] = await Promise.all([
this.renderHead?.(),
this.renderBody?.(),
this.renderEndOfBody?.(),
]);

const cspSource = webview.cspSource;
const cspNonce = randomBytes(16).toString('base64');
const root = webview.asWebviewUri(Container.context.extensionUri).toString();

const html = content
.replace(/#{(head|body|endOfBody)}/i, (_substring, token) => {
switch (token) {
case 'head':
return head ?? '';
case 'body':
return body ?? '';
case 'endOfBody':
return endOfBody ?? '';
default:
return '';
}
})
.replace(/#{(cspSource|cspNonce|root)}/g, (substring, token) => {
switch (token) {
case 'cspSource':
return cspSource;
case 'cspNonce':
return cspNonce;
case 'root':
return root;
default:
return '';
}
});

return html;
}
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/welcomeWebview.ts
Expand Up @@ -25,7 +25,7 @@ export class WelcomeWebview extends WebviewBase {
const bootstrap: WelcomeState = {
config: Container.config,
};
return `<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
return `<script type="text/javascript" nonce="#{cspNonce}">window.bootstrap = ${JSON.stringify(
bootstrap,
)};</script>`;
}
Expand Down
67 changes: 29 additions & 38 deletions webpack.config.js
Expand Up @@ -8,9 +8,8 @@
/* eslint-disable @typescript-eslint/prefer-optional-chain */
'use strict';
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const CspHtmlPlugin = require('csp-html-webpack-plugin');
const esbuild = require('esbuild');
Expand All @@ -20,6 +19,7 @@ const HtmlPlugin = require('html-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

class InlineChunkHtmlPlugin {
constructor(htmlPlugin, patterns) {
Expand Down Expand Up @@ -232,17 +232,32 @@ function getExtensionConfig(mode, env) {
function getWebviewsConfig(mode, env) {
const basePath = path.join(__dirname, 'src', 'webviews', 'apps');

const cspPolicy = {
'default-src': "'none'",
'img-src': ['#{cspSource}', 'https:', 'data:'],
'script-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
'style-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
'font-src': ['#{cspSource}'],
};

if (mode !== 'production') {
cspPolicy['script-src'].push("'unsafe-eval'");
}
const cspHtmlPlugin = new CspHtmlPlugin(
{
'default-src': "'none'",
'img-src': ['#{cspSource}', 'https:', 'data:'],
'script-src':
mode !== 'production'
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
: ['#{cspSource}', "'nonce-#{cspNonce}'"],
'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
'font-src': ['#{cspSource}'],
},
{
enabled: true,
hashingMethod: 'sha256',
hashEnabled: {
'script-src': true,
'style-src': true,
},
nonceEnabled: {
'script-src': true,
'style-src': true,
},
},
);
// Override the nonce creation so we can dynamically generate them at runtime
cspHtmlPlugin.createNonce = () => '#{cspNonce}';

/**
* @type WebpackConfig['plugins'] | any
Expand Down Expand Up @@ -280,14 +295,6 @@ function getWebviewsConfig(mode, env) {
filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
inject: true,
inlineSource: mode === 'production' ? '.css$' : undefined,
cspPlugin: {
enabled: true,
policy: cspPolicy,
nonceEnabled: {
'script-src': true,
'style-src': true,
},
},
minify:
mode === 'production'
? {
Expand All @@ -308,14 +315,6 @@ function getWebviewsConfig(mode, env) {
filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
inject: true,
inlineSource: mode === 'production' ? '.css$' : undefined,
cspPlugin: {
enabled: true,
policy: cspPolicy,
nonceEnabled: {
'script-src': true,
'style-src': true,
},
},
minify:
mode === 'production'
? {
Expand All @@ -336,14 +335,6 @@ function getWebviewsConfig(mode, env) {
filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
inject: true,
inlineSource: mode === 'production' ? '.css$' : undefined,
cspPlugin: {
enabled: true,
policy: cspPolicy,
nonceEnabled: {
'script-src': true,
'style-src': true,
},
},
minify:
mode === 'production'
? {
Expand All @@ -358,7 +349,7 @@ function getWebviewsConfig(mode, env) {
}
: false,
}),
new CspHtmlPlugin(),
cspHtmlPlugin,
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
new CopyPlugin({
patterns: [
Expand Down

0 comments on commit f77cf30

Please sign in to comment.