Skip to content

Commit 07ae55d

Browse files
committed
Add Enhanced Security Settings to Markdown Preview
Adds enhanced security settings for the markdown preview. The new flow disable all scripts within the preview itself. Users can enable scripts on a per workspace basis. When a markdown document that uses scripts is loaded, a warning is shown inside the document itself. This warning triggers a new security selector quick pick which allows users to enable or disable enahanced security in the workspace.
1 parent 7aac8ab commit 07ae55d

10 files changed

Lines changed: 264 additions & 60 deletions

File tree

extensions/markdown/media/csp.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
(function () {
9+
const settings = JSON.parse(document.getElementById('vscode-markdown-preview-data').getAttribute('data-settings'));
10+
const strings = JSON.parse(document.getElementById('vscode-markdown-preview-data').getAttribute('data-strings'));
11+
12+
let didShow = false;
13+
14+
document.addEventListener('securitypolicyviolation', () => {
15+
if (didShow) {
16+
return;
17+
}
18+
didShow = true;
19+
const args = [settings.previewUri];
20+
21+
const notification = document.createElement('a');
22+
notification.innerText = strings.cspAlertMessageText;
23+
notification.setAttribute('id', 'code-csp-warning');
24+
notification.setAttribute('title', strings.cspAlertMessageTitle);
25+
26+
notification.setAttribute('role', 'button');
27+
notification.setAttribute('aria-label', strings.cspAlertMessageLabel);
28+
notification.setAttribute('href', `command:markdown.showPreviewSecuritySelector?${encodeURIComponent(JSON.stringify(args))}`);
29+
30+
document.body.appendChild(notification);
31+
});
32+
}());

extensions/markdown/media/main.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
function scrollToRevealSourceLine(line) {
9090
const {previous, next} = getElementsForSourceLine(line);
9191
marker.update(previous && previous.element);
92-
if (previous && window.initialData.scrollPreviewWithEditorSelection) {
92+
if (previous && settings.scrollPreviewWithEditorSelection) {
9393
let scrollTo = 0;
9494
if (next) {
9595
// Between two elements. Go to percentage offset between them.
@@ -141,10 +141,11 @@
141141

142142
var scrollDisabled = true;
143143
var marker = new ActiveLineMarker();
144+
const settings = JSON.parse(document.getElementById('vscode-markdown-preview-data').getAttribute('data-settings'));
144145

145146
window.onload = () => {
146-
if (window.initialData.scrollPreviewWithEditorSelection) {
147-
const initialLine = +window.initialData.line;
147+
if (settings.scrollPreviewWithEditorSelection) {
148+
const initialLine = +settings.line;
148149
if (!isNaN(initialLine)) {
149150
setTimeout(() => {
150151
scrollDisabled = true;
@@ -172,7 +173,7 @@
172173
})(), false);
173174

174175
document.addEventListener('dblclick', event => {
175-
if (!window.initialData.doubleClickToSwitchToEditor) {
176+
if (!settings.doubleClickToSwitchToEditor) {
176177
return;
177178
}
178179

@@ -186,22 +187,22 @@
186187
const offset = event.pageY;
187188
const line = getEditorLineNumberForPageOffset(offset);
188189
if (!isNaN(line)) {
189-
const args = [window.initialData.source, line];
190+
const args = [settings.source, line];
190191
window.parent.postMessage({
191192
command: "did-click-link",
192193
data: `command:_markdown.didClick?${encodeURIComponent(JSON.stringify(args))}`
193194
}, "file://");
194195
}
195196
});
196197

197-
if (window.initialData.scrollEditorWithPreview) {
198+
if (settings.scrollEditorWithPreview) {
198199
window.addEventListener('scroll', throttle(() => {
199200
if (scrollDisabled) {
200201
scrollDisabled = false;
201202
} else {
202203
const line = getEditorLineNumberForPageOffset(window.scrollY);
203204
if (!isNaN(line)) {
204-
const args = [window.initialData.source, line];
205+
const args = [settings.source, line];
205206
window.parent.postMessage({
206207
command: "did-click-link",
207208
data: `command:_markdown.revealLine?${encodeURIComponent(JSON.stringify(args))}`

extensions/markdown/media/markdown.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@ body {
1111
word-wrap: break-word;
1212
}
1313

14+
#code-csp-warning {
15+
position: fixed;
16+
top: 0;
17+
right: 0;
18+
color: white;
19+
margin: 16px;
20+
text-align: center;
21+
font-size: 12px;
22+
font-family: sans-serif;
23+
background-color:#444444;
24+
cursor: pointer;
25+
padding: 6px;
26+
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
27+
}
28+
29+
#code-csp-warning:hover {
30+
text-decoration: none;
31+
background-color:#007acc;
32+
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
33+
}
34+
35+
1436
body.scrollBeyondLastLine {
1537
margin-bottom: calc(100vh - 22px);
1638
}

extensions/markdown/npm-shrinkwrap.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/markdown/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
"light": "./media/ViewSource.svg",
7070
"dark": "./media/ViewSource_inverse.svg"
7171
}
72+
},
73+
{
74+
"command": "markdown.showPreviewSecuritySelector",
75+
"title": "%markdown.showPreviewSecuritySelector.title%",
76+
"category": "Markdown"
7277
}
7378
],
7479
"menus": {
@@ -83,6 +88,10 @@
8388
"when": "resourceScheme == markdown",
8489
"command": "markdown.showSource",
8590
"group": "navigation"
91+
},
92+
{
93+
"when": "resourceScheme == markdown",
94+
"command": "markdown.showPreviewSecuritySelector"
8695
}
8796
],
8897
"explorer/context": [
@@ -183,7 +192,8 @@
183192
"highlight.js": "^9.3.0",
184193
"markdown-it": "^8.2.2",
185194
"markdown-it-named-headers": "0.0.4",
186-
"vscode-extension-telemetry": "^0.0.6"
195+
"vscode-extension-telemetry": "^0.0.6",
196+
"vscode-nls": "^2.0.2"
187197
},
188198
"devDependencies": {
189199
"@types/node": "^7.0.4"

extensions/markdown/package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
"markdown.previewFrontMatter.dec": "Sets how YAML front matter should be rendered in the markdown preview. 'hide' removes the front matter. Otherwise, the front matter is treated as markdown content.",
1111
"markdown.previewSide.title" : "Open Preview to the Side",
1212
"markdown.showSource.title" : "Show Source",
13-
"markdown.styles.dec": "A list of URLs or local paths to CSS style sheets to use from the markdown preview. Relative paths are interpreted relative to the folder open in the explorer. If there is no open folder, they are interpreted relative to the location of the markdown file. All '\\' need to be written as '\\\\'."
13+
"markdown.styles.dec": "A list of URLs or local paths to CSS style sheets to use from the markdown preview. Relative paths are interpreted relative to the folder open in the explorer. If there is no open folder, they are interpreted relative to the location of the markdown file. All '\\' need to be written as '\\\\'.",
14+
"markdown.showPreviewSecuritySelector.title": "Change Markdown Preview Security Settings"
1415
}

extensions/markdown/src/extension.ts

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import TelemetryReporter from 'vscode-extension-telemetry';
1111
import { MarkdownEngine } from './markdownEngine';
1212
import DocumentLinkProvider from './documentLinkProvider';
1313
import MDDocumentSymbolProvider from './documentSymbolProvider';
14-
import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile } from './previewContentProvider';
14+
import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile, ContentSecurityPolicyArbiter } from './previewContentProvider';
1515
import { TableOfContentsProvider } from './tableOfContentsProvider';
1616

17+
import * as nls from 'vscode-nls';
18+
19+
const localize = nls.loadMessageBundle();
20+
21+
1722
interface IPackageInfo {
1823
name: string;
1924
version: string;
@@ -25,6 +30,37 @@ interface OpenDocumentLinkArgs {
2530
fragment: string;
2631
}
2732

33+
enum PreviewSecuritySelection {
34+
None,
35+
DisableEnhancedSecurityForWorkspace,
36+
EnableEnhancedSecurityForWorkspace
37+
}
38+
39+
interface PreviewSecurityPickItem extends vscode.QuickPickItem {
40+
id: PreviewSecuritySelection;
41+
}
42+
43+
class ExtensionContentSecurityProlicyArbiter implements ContentSecurityPolicyArbiter {
44+
private readonly key = 'trusted_preview_workspace:';
45+
46+
constructor(
47+
private globalState: vscode.Memento
48+
) { }
49+
50+
public isEnhancedSecurityDisableForWorkspace(): boolean {
51+
return this.globalState.get<boolean>(this.key + vscode.workspace.rootPath, false);
52+
}
53+
54+
public addTrustedWorkspace(rootPath: string): Thenable<void> {
55+
return this.globalState.update(this.key + rootPath, true);
56+
}
57+
58+
public removeTrustedWorkspace(rootPath: string): Thenable<void> {
59+
return this.globalState.update(this.key + rootPath, false);
60+
}
61+
62+
}
63+
2864
var telemetryReporter: TelemetryReporter | null;
2965

3066
export function activate(context: vscode.ExtensionContext) {
@@ -34,9 +70,10 @@ export function activate(context: vscode.ExtensionContext) {
3470
context.subscriptions.push(telemetryReporter);
3571
}
3672

73+
const cspArbiter = new ExtensionContentSecurityProlicyArbiter(context.globalState);
3774
const engine = new MarkdownEngine();
3875

39-
const contentProvider = new MDDocumentContentProvider(engine, context);
76+
const contentProvider = new MDDocumentContentProvider(engine, context, cspArbiter);
4077
const contentProviderRegistration = vscode.workspace.registerTextDocumentContentProvider('markdown', contentProvider);
4178

4279
const symbolsProvider = new MDDocumentSymbolProvider(engine);
@@ -64,7 +101,7 @@ export function activate(context: vscode.ExtensionContext) {
64101
});
65102
}));
66103

67-
context.subscriptions.push(vscode.commands.registerCommand('_markdown.didClick', (uri, line) => {
104+
context.subscriptions.push(vscode.commands.registerCommand('_markdown.didClick', (uri: string, line) => {
68105
const sourceUri = vscode.Uri.parse(decodeURIComponent(uri));
69106
return vscode.workspace.openTextDocument(sourceUri)
70107
.then(document => vscode.window.showTextDocument(document))
@@ -100,6 +137,68 @@ export function activate(context: vscode.ExtensionContext) {
100137
}
101138
}));
102139

140+
context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewSecuritySelector', (resource: string | undefined) => {
141+
const workspacePath = vscode.workspace.rootPath || resource;
142+
if (!workspacePath) {
143+
return;
144+
}
145+
146+
let sourceUri: vscode.Uri | null = null;
147+
if (resource) {
148+
sourceUri = vscode.Uri.parse(decodeURIComponent(resource));
149+
}
150+
151+
if (!sourceUri && vscode.window.activeTextEditor) {
152+
const activeDocument = vscode.window.activeTextEditor.document;
153+
if (activeDocument.uri.scheme === 'markdown') {
154+
sourceUri = activeDocument.uri;
155+
} else {
156+
sourceUri = getMarkdownUri(activeDocument.uri);
157+
}
158+
}
159+
160+
vscode.window.showQuickPick<PreviewSecurityPickItem>(
161+
[
162+
{
163+
id: PreviewSecuritySelection.EnableEnhancedSecurityForWorkspace,
164+
label: localize(
165+
'preview.showPreviewSecuritySelector.disallowScriptsForWorkspaceTitle',
166+
'Disable script execution in markdown previews for this workspace'),
167+
description: '',
168+
detail: cspArbiter.isEnhancedSecurityDisableForWorkspace()
169+
? ''
170+
: localize('preview.showPreviewSecuritySelector.currentSelection', 'Current setting')
171+
}, {
172+
id: PreviewSecuritySelection.DisableEnhancedSecurityForWorkspace,
173+
label: localize(
174+
'preview.showPreviewSecuritySelector.allowScriptsForWorkspaceTitle',
175+
'Enable script execution in markdown previews for this workspace'),
176+
description: '',
177+
detail: cspArbiter.isEnhancedSecurityDisableForWorkspace()
178+
? localize('preview.showPreviewSecuritySelector.currentSelection', 'Current setting')
179+
: ''
180+
},
181+
], {
182+
placeHolder: localize('preview.showPreviewSecuritySelector.title', 'Change security settings for the Markdown preview'),
183+
}).then(selection => {
184+
if (!workspacePath) {
185+
return false;
186+
}
187+
switch (selection && selection.id) {
188+
case PreviewSecuritySelection.DisableEnhancedSecurityForWorkspace:
189+
return cspArbiter.addTrustedWorkspace(workspacePath).then(() => true);
190+
191+
case PreviewSecuritySelection.EnableEnhancedSecurityForWorkspace:
192+
return cspArbiter.removeTrustedWorkspace(workspacePath).then(() => true);
193+
}
194+
return false;
195+
}).then(shouldUpdate => {
196+
if (shouldUpdate && sourceUri) {
197+
contentProvider.update(sourceUri);
198+
}
199+
});
200+
}));
201+
103202
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => {
104203
if (isMarkdownFile(document)) {
105204
const uri = getMarkdownUri(document.uri);

0 commit comments

Comments
 (0)