Skip to content

Commit 0c85eb4

Browse files
committed
feat: improve security via iframe sandbox, less CSP mods, mdjs/fetching in background
1 parent 1560a56 commit 0c85eb4

13 files changed

+138
-74
lines changed

README.md

+18-12
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,25 @@ You can see live demos in
1111
- Preview of new issues
1212
- ... more is planned but not yet implemented
1313

14-
## Warning
14+
## Security
1515

16-
This extension modifies the CSP (Content Security Policy) for github.com with the following rules:
16+
This extension takes care of security by
17+
18+
- not executing any code without user action (e.g. requires a click of a button first)
19+
- shows demos/executes code within an iframe
20+
- that uses [sandbox](https://www.w3schools.com/tags/att_iframe_sandbox.asp) with the following settings `sandbox="allow-scripts"`
21+
- populates the iframe with a data uri
22+
- does not allow any requests (except unpkg) to got outside of the iframe
23+
24+
This prevents [all known attack vectors](https://github.com/open-wc/mdjs-viewer/issues/2). If you come up with new once please [report them](https://github.com/open-wc/mdjs-viewer/issues/new).
25+
26+
### Warning
27+
28+
In order to function this extension modifies the CSP (Content Security Policy) for github.com with the following rules:
1729

1830
- adds to script-src
19-
- `'unsafe-eval'` to allow [wasm](https://webassembly.org/) to analyze the code (moving action background will remove that need)
20-
- `'unsafe-inline'` to execute code within the mdjs iframe
21-
- `unpkg.com` to load dependencies from
22-
- adds to connect-src
23-
- `raw.githubusercontent.com` to fetch raw md content and package.json
24-
- adds to frame-src
25-
- `data:` to enable setting the content of the mdjs iframe
31+
- `'unsafe-inline'` to execute code blocks within the mdjs iframe
32+
- `unpkg.com` to load user dependencies from within the mdjs iframe
2633

2734
## Installation
2835

@@ -53,12 +60,11 @@ Finally we create an iframe with the content of the mdjs html and js output.
5360

5461
## Issues/ToDos/Future work
5562

56-
- Security review!!!
63+
- Even more security checks
5764
- Support relative imports from not root md files
5865
- Support relative links
5966
- Support github page switches (without manual reload)
60-
- Do the mdjs processing in the "background" (extension context or web worker) and not in the content window
6167
- Support in github pull request
6268
- Support npmjs
6369
- Support gitlab
64-
- Allow to define on which pages mdjs gets executed
70+
- Allow users to define on which urls mdjs-viewer gets loaded/executed

esm-loaders/esm-loader-content.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* global chrome */
22

33
// This file is a workaround to be able to use es modules
4-
const script = document.createElement('script');
5-
script.setAttribute('type', 'module');
6-
script.setAttribute('src', chrome.extension.getURL('./src/content.js'));
7-
const head = document.head || document.getElementsByTagName('head')[0] || document.documentElement;
8-
head.insertBefore(script, head.lastChild);
4+
(async () => {
5+
const src = chrome.extension.getURL('./src/content.js');
6+
const contentMain = await import(src);
7+
contentMain.main();
8+
})();

manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"js": ["esm-loaders/esm-loader-content.js"]
1111
}
1212
],
13-
"web_accessible_resources": ["src/*.js", "dist/index.js"],
13+
"web_accessible_resources": ["src/*.js", "dist/index.js", "dist/github-markdown.css"],
14+
"content_security_policy": "script-src 'self' 'unsafe-eval' unpkg.com; object-src 'self'",
1415
"background": {
1516
"page": "esm-loaders/esm-loader-background.html",
1617
"persistent": true

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"author": "open-wc",
1515
"homepage": "https://github.com/open-wc/mdjs-viewer/",
1616
"scripts": {
17-
"build": "rollup -c ./rollup.config.js",
17+
"build": "rollup -c ./rollup.config.js && cp node_modules/github-markdown-css/github-markdown.css ./dist",
1818
"lint:eslint": "eslint --ext .js,.html . --ignore-path .gitignore",
1919
"format:eslint": "eslint --ext .js,.html . --fix --ignore-path .gitignore",
2020
"lint:prettier": "prettier \"**/*.js\" --check --ignore-path .gitignore",
@@ -62,6 +62,7 @@
6262
]
6363
},
6464
"dependencies": {
65-
"@mdjs/core": "^0.1.0"
65+
"@mdjs/core": "^0.1.0",
66+
"github-markdown-css": "^4.0.0"
6667
}
6768
}

src/background.js

+41-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* global chrome */
2+
import { resolveToUnpkg, mdjsProcess } from '../dist/index.js';
23

34
// chrome.browserAction.onClicked.addListener(async tab => {
45
// // Send a message to the active tab
@@ -10,26 +11,53 @@
1011
// });
1112
// });
1213

14+
chrome.runtime.onMessage.addListener(({ action, ...options }, sender, sendResponse) => {
15+
if (action === 'mdjs+unpkg') {
16+
const { mdjs, pkgJson } = options;
17+
(async () => {
18+
const data = await mdjsProcess(mdjs);
19+
const executeCode = await resolveToUnpkg(data.jsCode, pkgJson);
20+
sendResponse({ jsCode: executeCode, html: data.html });
21+
})();
22+
}
23+
if (action === 'fetch') {
24+
const { url, fetchProcess } = options;
25+
(async () => {
26+
const response = await fetch(url);
27+
28+
// if HTTP-status is 200-299
29+
if (response.ok) {
30+
let data;
31+
switch (fetchProcess) {
32+
case 'text':
33+
data = await response.text();
34+
break;
35+
case 'json':
36+
data = await response.json();
37+
break;
38+
/* no default */
39+
}
40+
sendResponse({ ok: response.ok, data });
41+
} else {
42+
sendResponse({ ok: response.ok });
43+
}
44+
})();
45+
}
46+
47+
// mark this message as async
48+
return true;
49+
});
50+
1351
function onHeadersReceived(details) {
1452
const responseHeaders = [];
1553
for (const header of details.responseHeaders) {
1654
const { name } = header;
1755
let { value } = header;
1856
if (name.toLowerCase() === 'content-security-policy') {
1957
// adds to script-src
20-
// - `'unsafe-eval'` to allow [wasm](https://webassembly.org/) to analyze the code (moving action background will remove that need)
21-
// - `'unsafe-inline'` to execute code within the mdjs iframe
22-
// - `unpkg.com` to load dependencies from
23-
value = value.replace('script-src', "script-src 'unsafe-eval' 'unsafe-inline' unpkg.com");
24-
// adds to connect-src
25-
// - `raw.githubusercontent.com` to fetch raw md content and package.json
26-
value = value.replace('connect-src', 'connect-src raw.githubusercontent.com');
27-
// adds to frame-src
28-
// - `data:` to enable setting the content of the mdjs iframe
29-
value = value.replace('frame-src', 'frame-src data:');
30-
// adds to style-src
31-
// - raw.githubusercontent.com to fetch raw md content and package.json
32-
value = value.replace('style-src', 'style-src unpkg.com');
58+
// - `'unsafe-inline'` to execute code blocks within the mdjs iframe
59+
// - `unpkg.com` to load user dependencies from within the mdjs iframe
60+
value = value.replace('script-src', "script-src 'unsafe-inline' unpkg.com");
3361
}
3462
responseHeaders.push({ name, value });
3563
}

src/content.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { handleIssuePage } from './handleIssuePage.js';
22
import { handleMarkdownPage } from './handleMarkdownPage.js';
33

4-
async function main() {
4+
// main gets executed by the esm-loader
5+
export async function main() {
56
await handleIssuePage();
67
await handleMarkdownPage();
78

@@ -24,5 +25,3 @@ async function main() {
2425
}
2526
});
2627
}
27-
28-
main();

src/createViewer.js

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import { resolveToUnpkg, mdjsProcess } from '../dist/index.js';
1+
/* global chrome */
2+
3+
import { getFromBackground } from './getFromBackground.js';
24

35
let counter = 0;
46

57
export async function createViewer(mdjs, { type, width, height, pkgJson = {} }) {
68
counter += 1;
7-
const data = await mdjsProcess(mdjs);
8-
const executeCode = await resolveToUnpkg(data.jsCode, pkgJson);
9+
const data = await getFromBackground({
10+
action: 'mdjs+unpkg',
11+
mdjs,
12+
pkgJson,
13+
});
914
const iframeViewer = document.createElement('iframe');
1015
const iframeContent = `
1116
<meta name="viewport" content="width=device-width, initial-scale=1">
12-
<link rel="stylesheet" href="https://unpkg.com/github-markdown-css@4.0.0/github-markdown.css">
17+
<link rel="stylesheet" href="${chrome.extension.getURL('./dist/github-markdown.css')}">
1318
<style>
1419
body {
1520
margin: 0;
@@ -32,7 +37,7 @@ export async function createViewer(mdjs, { type, width, height, pkgJson = {} })
3237
parent.postMessage(JSON.stringify(data), '*');
3338
});
3439
observer.observe(document.body);
35-
${executeCode}
40+
${data.jsCode}
3641
</script>
3742
<body>
3843
<div class="markdown-body">
@@ -41,13 +46,10 @@ export async function createViewer(mdjs, { type, width, height, pkgJson = {} })
4146
</body>
4247
`;
4348

44-
iframeViewer.src = `data:text/html;charset=utf-8,${escape(iframeContent)}`;
45-
4649
const position =
4750
type === 'issue-show' || type === 'readme-show'
4851
? `position: absolute; left: 15px; top: 15px;`
4952
: '';
50-
5153
iframeViewer.setAttribute(
5254
'style',
5355
`
@@ -58,8 +60,13 @@ export async function createViewer(mdjs, { type, width, height, pkgJson = {} })
5860
min-height: ${height}px;
5961
`,
6062
);
63+
iframeViewer.setAttribute('sandbox', 'allow-scripts');
6164
iframeViewer.setAttribute('csp', "script-src unpkg.com 'unsafe-inline'; connect-src 'none'");
62-
6365
iframeViewer.setAttribute('iframe-viewer-id', counter);
66+
67+
// Uses a data url as when using srcdoc the iframe csp rules get ignored?
68+
// iframeViewer.setAttribute('srcdoc', iframeContent);
69+
iframeViewer.src = `data:text/html;charset=utf-8,${escape(iframeContent)}`;
70+
6471
return { iframe: iframeViewer, id: counter };
6572
}

src/getFromBackground.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* global chrome */
2+
3+
export function getFromBackground(options) {
4+
return new Promise(resolve => {
5+
chrome.runtime.sendMessage(options, response => {
6+
resolve(response);
7+
});
8+
});
9+
}

src/handleIssuePage.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable no-await-in-loop */
2-
import { isMdjsContent } from '../dist/index.js';
2+
import { isMdjsContentFork } from './isMdjsContentFork.js';
33
import { createViewer } from './createViewer.js';
44
import { createTriggerViewer } from './createTriggerViewer.js';
55

@@ -26,7 +26,7 @@ function handleCommentPreviews() {
2626
'.js-preview-body',
2727
);
2828

29-
if (textarea && textarea.value && isMdjsContent(textarea.value)) {
29+
if (textarea && textarea.value && isMdjsContentFork(textarea.value)) {
3030
childMutationToHappen(previewBody).then(() => {
3131
const dimensions = previewBody.getBoundingClientRect();
3232
createViewer(textarea.value, {
@@ -55,7 +55,7 @@ export async function handleIssuePage({ node = document } = {}) {
5555
const textarea = issueMsgNode.querySelector(
5656
'[name=issue_comment\\[body\\]], [name=issue\\[body\\]], [name=commit_comment\\[body\\]], [name=pull_request\\[body\\]]',
5757
);
58-
if (textarea && textarea.value && isMdjsContent(textarea.value)) {
58+
if (textarea && textarea.value && isMdjsContentFork(textarea.value)) {
5959
const issueBody = issueMsgNode.querySelector('.d-block.js-comment-body');
6060
issueBody.style.position = 'relative';
6161
const button = createTriggerViewer(textarea.value, {

src/handleMarkdownPage.js

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { isMdjsContent } from '../dist/index.js';
1+
import { isMdjsContentFork } from './isMdjsContentFork.js';
22
import { createTriggerViewer } from './createTriggerViewer.js';
3+
import { getFromBackground } from './getFromBackground.js';
34

45
function getPkgJsonUrl(urlString) {
56
const lengthToFirstSlashAfterBlob = urlString.indexOf('/', urlString.indexOf('blob/') + 5);
@@ -21,15 +22,23 @@ export async function handleMarkdownPage({ url = document.location.href, root =
2122
const fetchUrl = url.includes('/blob/') ? url : `${url}/blob/master/README.md`;
2223
const mdBody = root.querySelector('#readme .markdown-body');
2324
if (mdBody) {
24-
const responseMdjs = await fetch(getMdjsUrl(fetchUrl));
25-
const responsePkgJson = await fetch(getPkgJsonUrl(fetchUrl));
25+
const responseMdjs = await getFromBackground({
26+
action: 'fetch',
27+
fetchProcess: 'text',
28+
url: getMdjsUrl(fetchUrl),
29+
});
30+
const responsePkgJson = await getFromBackground({
31+
action: 'fetch',
32+
fetchProcess: 'json',
33+
url: getPkgJsonUrl(fetchUrl),
34+
});
2635

2736
// if HTTP-status is 200-299
2837
if (responseMdjs.ok && responsePkgJson.ok) {
29-
const text = await responseMdjs.text();
30-
if (isMdjsContent(text)) {
38+
const text = await responseMdjs.data;
39+
if (isMdjsContentFork(text)) {
3140
mdBody.style.position = 'relative';
32-
const pkgJson = await responsePkgJson.json();
41+
const pkgJson = await responsePkgJson.data;
3342
const viewer = createTriggerViewer(text, { type: 'readme-show', pkgJson });
3443
mdBody.appendChild(viewer);
3544
}

src/ideas/mdjsInBackground.js

-20
This file was deleted.

src/isMdjsContentFork.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Check if a given text uses any of the mdjs features
3+
*
4+
* @param {string} text
5+
* @returns {boolean}
6+
*/
7+
export function isMdjsContentFork(text) {
8+
if (!text) {
9+
return false;
10+
}
11+
switch (true) {
12+
case text.includes('```js script'):
13+
case text.includes('```js story'):
14+
case text.includes('```js preview-story'):
15+
return true;
16+
default:
17+
return false;
18+
}
19+
}

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3037,6 +3037,11 @@ github-markdown-css@^3.0.1:
30373037
resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-3.0.1.tgz#d08db1060d2e182025e0d07d547cfe2afed30205"
30383038
integrity sha512-9G5CIPsHoyk5ObDsb/H4KTi23J8KE1oDd4KYU51qwqeM+lKWAiO7abpSgCkyWswgmSKBiuE7/4f8xUz7f2qAiQ==
30393039

3040+
github-markdown-css@^4.0.0:
3041+
version "4.0.0"
3042+
resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-4.0.0.tgz#be9f4caf7a389228d4c368336260ffc909061f35"
3043+
integrity sha512-mH0bcIKv4XAN0mQVokfTdKo2OD5K8WJE9+lbMdM32/q0Ie5tXgVN/2o+zvToRMxSTUuiTRcLg5hzkFfOyBYreg==
3044+
30403045
github-slugger@^1.1.1:
30413046
version "1.3.0"
30423047
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9"

0 commit comments

Comments
 (0)