Browse Source

Adds better webview security

main
Eric Amodio 3 years ago
parent
commit
f77cf30643
9 changed files with 102 additions and 69 deletions
  1. +4
    -0
      CHANGELOG.md
  2. +1
    -1
      src/webviews/apps/rebase/rebase.html
  3. +1
    -1
      src/webviews/apps/settings/settings.html
  4. +1
    -1
      src/webviews/apps/welcome/welcome.html
  5. +28
    -11
      src/webviews/rebaseEditor.ts
  6. +1
    -1
      src/webviews/settingsWebview.ts
  7. +36
    -15
      src/webviews/webviewBase.ts
  8. +1
    -1
      src/webviews/welcomeWebview.ts
  9. +29
    -38
      webpack.config.js

+ 4
- 0
CHANGELOG.md View File

@ -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

+ 1
- 1
src/webviews/apps/rebase/rebase.html View File

@ -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');

+ 1
- 1
src/webviews/apps/settings/settings.html View File

@ -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');

+ 1
- 1
src/webviews/apps/welcome/welcome.html View File

@ -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');

+ 28
- 11
src/webviews/rebaseEditor.ts View File

@ -1,4 +1,5 @@
'use strict';
import { randomBytes } from 'crypto';
import { TextDecoder } from 'util';
import {
CancellationToken,
@ -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;
}

+ 1
- 1
src/webviews/settingsWebview.ts View File

@ -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>`;
}

+ 36
- 15
src/webviews/webviewBase.ts View File

@ -1,4 +1,5 @@
'use strict';
import { randomBytes } from 'crypto';
import { TextDecoder } from 'util';
import {
commands,
@ -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;
}

+ 1
- 1
src/webviews/welcomeWebview.ts View File

@ -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>`;
}

+ 29
- 38
webpack.config.js View File

@ -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');
@ -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) {
@ -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
@ -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'
? {
@ -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'
? {
@ -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'
? {
@ -358,7 +349,7 @@ function getWebviewsConfig(mode, env) {
}
: false,
}),
new CspHtmlPlugin(),
cspHtmlPlugin,
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
new CopyPlugin({
patterns: [

Loading…
Cancel
Save