Skip to content

Commit f77cf30

Browse files
committed
Adds better webview security
1 parent afa3252 commit f77cf30

File tree

9 files changed

+102
-69
lines changed

9 files changed

+102
-69
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1111
- Adds new _Open Previous Changes with Working File_ command to commit files in views — closes [#1529](https://github.com/eamodio/vscode-gitlens/issues/1529)
1212
- Adopts new vscode `createStatusBarItem` API to allow for independent toggling — closes [#1543](https://github.com/eamodio/vscode-gitlens/issues/1543)
1313

14+
### Changed
15+
16+
- Dynamically generates hashes and nonces for webview script and style tags for better
17+
1418
### Fixed
1519

1620
- Fixes [#1432](https://github.com/eamodio/vscode-gitlens/issues/1432) - Unhandled Timeout Promise

src/webviews/apps/rebase/rebase.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<style nonce="Z2l0bGVucy1ib290c3RyYXA=">
5+
<style nonce="#{cspNonce}">
66
@font-face {
77
font-family: 'codicon';
88
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');

src/webviews/apps/settings/settings.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<style nonce="Z2l0bGVucy1ib290c3RyYXA=">
5+
<style nonce="#{cspNonce}">
66
@font-face {
77
font-family: 'codicon';
88
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');

src/webviews/apps/welcome/welcome.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<style nonce="Z2l0bGVucy1ib290c3RyYXA=">
5+
<style nonce="#{cspNonce}">
66
@font-face {
77
font-family: 'codicon';
88
src: url('#{root}/dist/webviews/codicon.ttf?669d352dfabff8f6eaa466c8ae820e43') format('truetype');

src/webviews/rebaseEditor.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
import { randomBytes } from 'crypto';
23
import { TextDecoder } from 'util';
34
import {
45
CancellationToken,
@@ -476,18 +477,34 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
476477
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', 'rebase.html');
477478
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));
478479

479-
let html = content
480-
.replace(/#{cspSource}/g, context.panel.webview.cspSource)
481-
.replace(/#{root}/g, context.panel.webview.asWebviewUri(Container.context.extensionUri).toString());
482-
483480
const bootstrap = await this.parseState(context);
484-
485-
html = html.replace(
486-
/#{endOfBody}/i,
487-
`<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
488-
bootstrap,
489-
)};</script>`,
490-
);
481+
const cspSource = context.panel.webview.cspSource;
482+
const cspNonce = randomBytes(16).toString('base64');
483+
const root = context.panel.webview.asWebviewUri(Container.context.extensionUri).toString();
484+
485+
const html = content
486+
.replace(/#{(head|body|endOfBody)}/i, (_substring, token) => {
487+
switch (token) {
488+
case 'endOfBody':
489+
return `<script type="text/javascript" nonce="#{cspNonce}">window.bootstrap = ${JSON.stringify(
490+
bootstrap,
491+
)};</script>`;
492+
default:
493+
return '';
494+
}
495+
})
496+
.replace(/#{(cspSource|cspNonce|root)}/g, (substring, token) => {
497+
switch (token) {
498+
case 'cspSource':
499+
return cspSource;
500+
case 'cspNonce':
501+
return cspNonce;
502+
case 'root':
503+
return root;
504+
default:
505+
return '';
506+
}
507+
});
491508

492509
return html;
493510
}

src/webviews/settingsWebview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class SettingsWebview extends WebviewBase {
9999
scope: 'user',
100100
scopes: scopes,
101101
};
102-
return `<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
102+
return `<script type="text/javascript" nonce="#{cspNonce}">window.bootstrap = ${JSON.stringify(
103103
bootstrap,
104104
)};</script>`;
105105
}

src/webviews/webviewBase.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
import { randomBytes } from 'crypto';
23
import { TextDecoder } from 'util';
34
import {
45
commands,
@@ -325,21 +326,41 @@ export abstract class WebviewBase implements Disposable {
325326
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', this.filename);
326327
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));
327328

328-
let html = content
329-
.replace(/#{cspSource}/g, webview.cspSource)
330-
.replace(/#{root}/g, webview.asWebviewUri(Container.context.extensionUri).toString());
331-
332-
if (this.renderHead != null) {
333-
html = html.replace(/#{head}/i, await this.renderHead());
334-
}
335-
336-
if (this.renderBody != null) {
337-
html = html.replace(/#{body}/i, await this.renderBody());
338-
}
339-
340-
if (this.renderEndOfBody != null) {
341-
html = html.replace(/#{endOfBody}/i, await this.renderEndOfBody());
342-
}
329+
const [head, body, endOfBody] = await Promise.all([
330+
this.renderHead?.(),
331+
this.renderBody?.(),
332+
this.renderEndOfBody?.(),
333+
]);
334+
335+
const cspSource = webview.cspSource;
336+
const cspNonce = randomBytes(16).toString('base64');
337+
const root = webview.asWebviewUri(Container.context.extensionUri).toString();
338+
339+
const html = content
340+
.replace(/#{(head|body|endOfBody)}/i, (_substring, token) => {
341+
switch (token) {
342+
case 'head':
343+
return head ?? '';
344+
case 'body':
345+
return body ?? '';
346+
case 'endOfBody':
347+
return endOfBody ?? '';
348+
default:
349+
return '';
350+
}
351+
})
352+
.replace(/#{(cspSource|cspNonce|root)}/g, (substring, token) => {
353+
switch (token) {
354+
case 'cspSource':
355+
return cspSource;
356+
case 'cspNonce':
357+
return cspNonce;
358+
case 'root':
359+
return root;
360+
default:
361+
return '';
362+
}
363+
});
343364

344365
return html;
345366
}

src/webviews/welcomeWebview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class WelcomeWebview extends WebviewBase {
2525
const bootstrap: WelcomeState = {
2626
config: Container.config,
2727
};
28-
return `<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
28+
return `<script type="text/javascript" nonce="#{cspNonce}">window.bootstrap = ${JSON.stringify(
2929
bootstrap,
3030
)};</script>`;
3131
}

webpack.config.js

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
/* eslint-disable @typescript-eslint/prefer-optional-chain */
99
'use strict';
1010
const path = require('path');
11-
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
12-
const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
1311
const CircularDependencyPlugin = require('circular-dependency-plugin');
12+
const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
1413
const CopyPlugin = require('copy-webpack-plugin');
1514
const CspHtmlPlugin = require('csp-html-webpack-plugin');
1615
const esbuild = require('esbuild');
@@ -20,6 +19,7 @@ const HtmlPlugin = require('html-webpack-plugin');
2019
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
2120
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
2221
const TerserPlugin = require('terser-webpack-plugin');
22+
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
2323

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

235-
const cspPolicy = {
236-
'default-src': "'none'",
237-
'img-src': ['#{cspSource}', 'https:', 'data:'],
238-
'script-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
239-
'style-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
240-
'font-src': ['#{cspSource}'],
241-
};
242-
243-
if (mode !== 'production') {
244-
cspPolicy['script-src'].push("'unsafe-eval'");
245-
}
235+
const cspHtmlPlugin = new CspHtmlPlugin(
236+
{
237+
'default-src': "'none'",
238+
'img-src': ['#{cspSource}', 'https:', 'data:'],
239+
'script-src':
240+
mode !== 'production'
241+
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
242+
: ['#{cspSource}', "'nonce-#{cspNonce}'"],
243+
'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
244+
'font-src': ['#{cspSource}'],
245+
},
246+
{
247+
enabled: true,
248+
hashingMethod: 'sha256',
249+
hashEnabled: {
250+
'script-src': true,
251+
'style-src': true,
252+
},
253+
nonceEnabled: {
254+
'script-src': true,
255+
'style-src': true,
256+
},
257+
},
258+
);
259+
// Override the nonce creation so we can dynamically generate them at runtime
260+
cspHtmlPlugin.createNonce = () => '#{cspNonce}';
246261

247262
/**
248263
* @type WebpackConfig['plugins'] | any
@@ -280,14 +295,6 @@ function getWebviewsConfig(mode, env) {
280295
filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
281296
inject: true,
282297
inlineSource: mode === 'production' ? '.css$' : undefined,
283-
cspPlugin: {
284-
enabled: true,
285-
policy: cspPolicy,
286-
nonceEnabled: {
287-
'script-src': true,
288-
'style-src': true,
289-
},
290-
},
291298
minify:
292299
mode === 'production'
293300
? {
@@ -308,14 +315,6 @@ function getWebviewsConfig(mode, env) {
308315
filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
309316
inject: true,
310317
inlineSource: mode === 'production' ? '.css$' : undefined,
311-
cspPlugin: {
312-
enabled: true,
313-
policy: cspPolicy,
314-
nonceEnabled: {
315-
'script-src': true,
316-
'style-src': true,
317-
},
318-
},
319318
minify:
320319
mode === 'production'
321320
? {
@@ -336,14 +335,6 @@ function getWebviewsConfig(mode, env) {
336335
filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
337336
inject: true,
338337
inlineSource: mode === 'production' ? '.css$' : undefined,
339-
cspPlugin: {
340-
enabled: true,
341-
policy: cspPolicy,
342-
nonceEnabled: {
343-
'script-src': true,
344-
'style-src': true,
345-
},
346-
},
347338
minify:
348339
mode === 'production'
349340
? {
@@ -358,7 +349,7 @@ function getWebviewsConfig(mode, env) {
358349
}
359350
: false,
360351
}),
361-
new CspHtmlPlugin(),
352+
cspHtmlPlugin,
362353
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
363354
new CopyPlugin({
364355
patterns: [

0 commit comments

Comments
 (0)