vscode/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts
Matt Bierner 474d4951d8
Switch to dompurify for sanitizing markdown content (#131950)
* Switch to dompurify for sanitizing markdown content

Switches us from using `insane` to instead use `dompurify`, which seems to be better maintained and also has some nice features, such as built-in trusted types support

I've tried to port over our existing sanitizer settings as best as possible, but there's not always a 1:1 mapping between how insane works and how dompurify does. I'd like to get this change in early in the iteration to catch potential regressions

* Remove logging and renaming param

* Move dompurify to browser layer

* Fixing tests and how we check valid attributes

* Allow innerhtml in specific files

* Use isEqualNode instead of checking innerHTML directly

innerHTML can return different results on different browsers. Use `isEqualNode` instead

* Reapply fix for trusted types

* Enable ALLOW_UNKNOWN_PROTOCOLS

I beleive this is required since we allow links to commands and loading images over remote

* in -> of

* Fix check of protocol

* Enable two more safe tags
2021-09-03 12:17:02 -07:00

229 lines
5.6 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dompurify from 'vs/base/browser/dompurify/dompurify';
import * as marked from 'vs/base/common/marked/marked';
import { Schemas } from 'vs/base/common/network';
import { ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/modes';
import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export const DEFAULT_MARKDOWN_STYLES = `
body {
padding: 10px 20px;
line-height: 22px;
max-width: 882px;
margin: 0 auto;
}
body *:last-child {
margin-bottom: 0;
}
img {
max-width: 100%;
max-height: 100%;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
h1, h2, h3 {
font-weight: normal;
}
table {
border-collapse: collapse;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top-width: 1px;
border-top-style: solid;
}
blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
code {
font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
}
pre code {
font-family: var(--vscode-editor-font-family);
font-weight: var(--vscode-editor-font-weight);
font-size: var(--vscode-editor-font-size);
line-height: 1.5;
}
code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
.monaco-tokenized-source {
white-space: pre;
}
/** Theming */
.vscode-light code > div {
background-color: rgba(220, 220, 220, 0.4);
}
.vscode-dark code > div {
background-color: rgba(10, 10, 10, 0.4);
}
.vscode-high-contrast code > div {
background-color: rgb(0, 0, 0);
}
.vscode-high-contrast h1 {
border-color: rgb(0, 0, 0);
}
.vscode-light table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vscode-dark table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.vscode-light h1,
.vscode-light hr,
.vscode-light table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vscode-dark h1,
.vscode-dark hr,
.vscode-dark table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}
`;
const allowedProtocols = [Schemas.http, Schemas.https, Schemas.command];
function sanitize(documentContent: string): string {
// https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html
dompurify.addHook('afterSanitizeAttributes', (node) => {
// build an anchor to map URLs to
const anchor = document.createElement('a');
// check all href/src attributes for validity
for (const attr in ['href', 'src']) {
if (node.hasAttribute(attr)) {
anchor.href = node.getAttribute(attr) as string;
if (!allowedProtocols.includes(anchor.protocol)) {
node.removeAttribute(attr);
}
}
}
});
try {
return dompurify.sanitize(documentContent, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote', 'dl', 'dt',
'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details',
'caption', 'figure', 'figcaption', 'abbr', 'bdo', 'cite', 'dfn', 'mark', 'small', 'span', 'time', 'wbr', 'checkbox', 'checklist', 'vertically-centered'
],
ALLOWED_ATTR: [
'href', 'data-href', 'data-command', 'target', 'title', 'name', 'src', 'alt', 'class', 'id', 'role', 'tabindex', 'style', 'data-code',
'width', 'height', 'align', 'x-dispatch',
'required', 'checked', 'placeholder', 'on-checked', 'checked-on',
],
});
} finally {
dompurify.removeHook('afterSanitizeAttributes');
}
}
/**
* Renders a string of markdown as a document.
*
* Uses VS Code's syntax highlighting code blocks.
*/
export async function renderMarkdownDocument(
text: string,
extensionService: IExtensionService,
modeService: IModeService,
shouldSanitize: boolean = true,
): Promise<string> {
const highlight = (code: string, lang: string, callback: ((error: any, code: string) => void) | undefined): any => {
if (!callback) {
return code;
}
extensionService.whenInstalledExtensionsRegistered().then(async () => {
let support: ITokenizationSupport | undefined;
const modeId = modeService.getModeIdForLanguageName(lang);
if (modeId) {
modeService.triggerMode(modeId);
support = await TokenizationRegistry.getPromise(modeId) ?? undefined;
}
callback(null, `<code>${tokenizeToString(code, support)}</code>`);
});
return '';
};
return new Promise<string>((resolve, reject) => {
marked(text, { highlight }, (err, value) => err ? reject(err) : resolve(value));
}).then(raw => {
if (shouldSanitize) {
return sanitize(raw);
} else {
return raw;
}
});
}