Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revised content security policy and added WebViewContentType.URL for url webviews #610

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions extensions/src/evil/assets/evil.web-view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<body>
<script>
try {
// Note: child iframes can always tell window !== window.top, but they cannot access stuff
// on window.top unless they are same origin
window.top.papi.logger.error(
'<<BAD>> EVIL WEB VIEW FILE HAS SAME ORIGIN ACCESS TO window.top AND LOGGED THIS ERROR THROUGH THE PAPI ON ITS OWN',
);
} catch (e) {
if (!e.toString().includes('cross-origin frame'))
console.warn(
`Evil web view file failed to reach parent, but it was not because it is cross-origin. Investigate! Error: ${e}`,
);
else {
// Good! This iframe is not considered to be same origin as parent
// No need to log good stuff unless we're testing
// console.log(`Evil: good error on attempting cross-origin access window.top: ${e}`)
}
}
try {
// Note: child iframes can always tell window !== window.parent, but they cannot access stuff
// on window.parent unless they are same origin
window.parent.papi.logger.error(
'<<BAD>> EVIL WEB VIEW FILE HAS SAME ORIGIN ACCESS TO window.parent AND LOGGED THIS ERROR THROUGH THE PAPI ON ITS OWN',
);
} catch (e) {
if (!e.toString().includes('cross-origin frame'))
console.warn(
`Evil web view file failed to reach parent, but it was not because it is cross-origin. Investigate! Error: ${e}`,
);
else {
// Good! This iframe is not considered to be same origin as parent
// No need to log good stuff unless we're testing
// console.log(`Evil: good error on attempting cross-origin access window.parent: ${e}`)
}
}

// Note: our CSP is independent of `index.ejs`'s, so we can run whatever scripts we want
eval('1 + 1');
</script>
</body>
<div>
This evil webview comes from a file! It is served at
`papi-extension:evil/assets/evil.web-view.html`. It is trying to do bad things. If you see an
error in the console like '&lt;&lt;BAD&gt;&gt; EVIL WEB VIEW FILE...', it did bad things. But if
you see this message without seeing errors like that, it probably wasn't able to do bad things!
:)
</div>
</html>
208 changes: 178 additions & 30 deletions extensions/src/evil/evil.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,75 @@ const papi = require('papi-backend');

const { logger } = papi;

// This is here because we can't bundle a webview in due to webpack not bundling the evil extension
const EVIL_WEBVIEW = `
<!DOCTYPE html>
<html>
<body>
<script>
// Try to create an iframe with less strict sandboxing - allow-modals (better test than no
// sandboxing at all)
const unsandboxedId = "evil-unsandboxed-iframe";
const unsandboxedIFrame = window.top.document.createElement('iframe');
unsandboxedIFrame.id = unsandboxedId;
unsandboxedIFrame.srcdoc = \`<html><script>alert(
"<<BAD>> evil created a new iframe with sandbox 'allow-modals'!")<\\/script><body>This is
evil's new iframe with sandbox 'allow-modals'</body></html>\`;
unsandboxedIFrame.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-modals');

// If one of these evil iframes already existed, replace it. Otherwise create a new one
papi.logger.warn(
'Evil is trying to execute code with higher privileges as a test! You should see one more warning soon after this. Only these two warnings are expected.'
);
const oldIFrame = window.top.document.getElementById(unsandboxedId);
if (oldIFrame != null) oldIFrame.replaceWith(unsandboxedIFrame);
else window.top.document.body.appendChild(unsandboxedIFrame);
// Try to create a src iframe on the parent window - this would let us escape our CSP
try {
// Try to create a \`src\` iframe with less strict sandboxing - allow-same-origin (not allowed
// on \`src\` iframes). Note we are also providing \`srcdoc\`, but browsers fall back to
// \`src\` if they do not support \`srcdoc\`, so any iframe with \`src\` specified is treated
// as a \`src\` iframe
const unsandboxedSrcId = "evil-unsandboxed-src-iframe";
const unsandboxedSrcIFrame = window.top.document.createElement('iframe');
unsandboxedSrcIFrame.id = unsandboxedSrcId;
unsandboxedSrcIFrame.src = "https://example.com/";
unsandboxedSrcIFrame.srcdoc = \`<html><body>&lt;&lt;BAD&gt;&gt; This is evil's new src iframe with sandbox
'allow-same-origin'. Please report this!</body></html>\`;
unsandboxedSrcIFrame.setAttribute('sandbox', 'allow-same-origin allow-scripts');

// We now have two layers preventing this iframe from being created. Uncomment this warning
// if you want to test the MutationObserver. But this iframe should be completely rejected
// by the monkey-patched document.createElement, so this warning is not needed at the moment
/* papi.logger.warn(
'Evil is trying to execute code with higher privileges as a test of src iframes! You should see three more warnings soon after this. Only these four warnings are expected.'
); */

// If one of these evil src iframes already existed, replace it. Otherwise create a new one
const oldSrcIFrame = window.top.document.getElementById(unsandboxedSrcId);
if (oldSrcIFrame != null) oldSrcIFrame.replaceWith(unsandboxedSrcIFrame);
else window.top.document.body.appendChild(unsandboxedSrcIFrame);

papi.logger.error('<<BAD>> Evil successfully created a src iframe on the parent window!')
} catch (e) {
// No need to log good stuff unless we're testing
// papi.logger.info(\`Evil: Good error on creating src iframe outside its frame: \${e.message}\`);
}

// Try to create a srcdoc iframe on the parent window - this would let us escape our CSP
try {
// Try to create a \`srcdoc\` iframe with less strict sandboxing - allow-modals (better test than no
// sandboxing at all) - inside a div to make sure the MutationObserver is watching recursively
const unsandboxedId = "evil-unsandboxed-iframe-div";
const unsandboxedIFrameDiv = window.top.document.createElement('div');
unsandboxedIFrameDiv.id = unsandboxedId;
const unsandboxedIFrame = window.top.document.createElement('iframe');
unsandboxedIFrame.srcdoc = \`<html><script>alert(
"<<BAD>> evil created a new iframe with sandbox 'allow-modals'!")<\\/script><body>This is
evil's new iframe with sandbox 'allow-modals'</body></html>\`;
unsandboxedIFrame.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-modals');
unsandboxedIFrameDiv.appendChild(unsandboxedIFrame);

// We now have two layers preventing this iframe from being created. Uncomment this warning
// if you want to test the MutationObserver. But this iframe should be completely rejected
// by the monkey-patched document.createElement, so this warning is not needed at the moment
/* papi.logger.warn(
'Evil is trying to execute code with higher privileges as a test of srcdoc iframes! You should see two more warnings soon after this. Only these four warnings (including the previous one) are expected.'
); */

// If one of these evil iframes already existed, replace it. Otherwise create a new one
const oldIFrameDiv = window.top.document.getElementById(unsandboxedId);
if (oldIFrameDiv != null) oldIFrameDiv.replaceWith(unsandboxedIFrameDiv);
else window.top.document.body.appendChild(unsandboxedIFrameDiv);

papi.logger.error('<<BAD>> Evil successfully created a srcdoc iframe on the parent window!')
} catch (e) {
// No need to log good stuff unless we're testing
// papi.logger.info(\`Evil: Good error on creating srcdoc iframe outside its frame: \${e.message}\`);
}

// Try to create a script outside the iframe that runs arbitrary code
// This would mean iframes can break out of their sandboxing and CSP by executing code that
Expand All @@ -39,7 +86,20 @@ const EVIL_WEBVIEW = `
"alert('<<BAD>> evil created a script outside its iframe');window.location = 'https://example.com'";
window.top.document.body.appendChild(unsandboxedScript);
} catch (e) {
papi.logger.info(\`Evil: Good error on creating script outside its frame: \${e.message}\`);
// No need to log good stuff unless we're testing
// papi.logger.info(\`Evil: Good error on creating script outside its frame: \${e.message}\`);
}

// Try to create an anchor outside the iframe that allows navigation
// This would mean iframes could create a link to navigate to a different page.
try {
const unsafeAnchor = window.top.document.createElement('a');
unsafeAnchor.href ='https://example.com';
unsafeAnchor.textContent = '<<BAD>> This link is from the evil webview!'
window.top.document.body.appendChild(unsafeAnchor);
} catch (e) {
// No need to log good stuff unless we're testing
// papi.logger.info(\`Evil: Good error on creating script outside its frame: \${e.message}\`);
}

// Try to create an image outside the iframe with arbitrary code execution in it
Expand All @@ -54,14 +114,73 @@ const EVIL_WEBVIEW = `
// window.top.document.body.appendChild(imgWithAttributeScript);
*/

// Try to create a modal through window.top.alert
try {
window.top.alert('<<BAD>> Evil could create a modal through window.top.alert!');
} catch (e) {
// No need to log good stuff unless we're testing
//papi.logger.info(\`Evil: Good error on running window.top.alert: \${e.message}\`);
}

// Try to create a modal through window.top.confirm
try {
window.top.confirm('<<BAD>> Evil could create a modal through window.top.confirm!');
} catch (e) {
// No need to log good stuff unless we're testing
//papi.logger.info(\`Evil: Good error on running window.top.confirm: \${e.message}\`);
}

// Try to create a modal through window.top.print
try {
window.top.print('<<BAD>> Evil could create a modal through window.top.print!');
} catch (e) {
// No need to log good stuff unless we're testing
//papi.logger.info(\`Evil: Good error on running window.top.print: \${e.message}\`);
}

// Try to create a modal through window.top.prompt
try {
window.top.prompt('<<BAD>> Evil could create a modal through window.top.prompt!');
} catch (e) {
// No need to log good stuff unless we're testing
//papi.logger.info(\`Evil: Good error on running window.top.prompt: \${e.message}\`);
}

// Try to create a popup through window.top.open
try {
window.top.open('<<BAD>> Evil could create a popup through window.top.open!');
} catch (e) {
// No need to log good stuff unless we're testing
//papi.logger.info(\`Evil: Good error on running window.top.open: \${e.message}\`);
}

// Try to create a popup through window.top.showModalDialog
try {
window.top.showModalDialog('<<BAD>> Evil could create a popup through window.top.showModalDialog!');
} catch (e) {
// No need to log good stuff unless we're testing
//papi.logger.info(\`Evil: Good error on running window.top.showModalDialog: \${e.message}\`);
}

// Note: we are using this sourceURL in web-view.service.ts, so keep it up-to-date with this
//# sourceURL=evil.web-view.html
</script>
<div>
This evil webview is trying to do bad things. If you see a new iframe on the side of the
screen or see modals about evil, it did bad things. But if you see this message without seeing
modals, it probably wasn't able to do bad things! 🎉
This evil webview is trying to do bad things. If you see a new iframe or link on the side of
the screen or see modals about evil, it did bad things. But if you see this message without
seeing modals, it probably wasn't able to do bad things! 🎉
</div>
<div>
Below, you should see an iframe with another evil webview code that also should fail to do bad
things:
</div>
<iframe src="papi-extension://evil/assets/evil.web-view.html"></iframe>
<!--
Uncomment this to test that iframes within iframes are restricted by the sandbox of their parent
This is commented out because it causes a sandbox error to show up in the console, and we don't
want to distract people with it.
<iframe srcdoc="<!DOCTYPE html><html><body><script>try { window.top.location='https://example.com'; } catch (e) {}</script></body></html>"></iframe>
-->
</body>
</html>
`;
Expand All @@ -74,6 +193,21 @@ const evilWebViewProvider = {
title: 'Evil',
contentType: 'html',
content: EVIL_WEBVIEW,
allowedFrameSources: ['papi-extension:'],
};
},
};

const evilFileWebViewProvider = {
webViewType: 'evil.evilFile',
async getWebView(savedWebView) {
return {
...savedWebView,
title: 'Evil File',
contentType: 'url',
content: 'papi-extension://evil/assets/evil.web-view.html',
allowScripts: true,
allowedFrameSources: ['papi-extension://evil/.+'],
};
},
};
Expand All @@ -86,21 +220,24 @@ async function tryImports() {
const fs = require('fs');
logger.error(`Evil: <<BAD>> Successfully imported fs! fs.readFileSync = ${fs.readFileSync}`);
} catch (e) {
logger.info(`Evil: Good error on require fs: ${e.message}`);
// No need to log good stuff unless we're testing
// logger.info(`Evil: Good error on require fs: ${e.message}`);
}

try {
// This will be blocked and will suggest the papi.fetch api.
const https = require('https');
logger.error(`Evil: <<BAD>> Successfully imported https! ${JSON.stringify(https)}`);
} catch (e) {
logger.info(`Evil: Good error on require https: ${e.message}`);
// No need to log good stuff unless we're testing
// logger.info(`Evil: Good error on require https: ${e.message}`);
}

try {
// This should always work because `fetch` is replaced with `papi.fetch`.
await fetch('https://www.example.com');
logger.info('Evil: Good - fetch is working.');
// No need to log good stuff unless we're testing
// logger.info('Evil: Good - fetch is working.');
} catch (e) {
logger.error(`Evil: <<BAD>> error on fetch! ${e}`);
}
Expand All @@ -111,7 +248,8 @@ async function tryImports() {
const xhr = new XMLHttpRequest();
logger.error(`Evil: <<BAD>> Successfully created an XMLHttpRequest!`);
} catch (e) {
logger.info(`Evil: Good error on XMLHttpRequest! ${e}`);
// No need to log good stuff unless we're testing
// logger.info(`Evil: Good error on XMLHttpRequest! ${e}`);
}

try {
Expand All @@ -120,7 +258,8 @@ async function tryImports() {
const webSocket = new WebSocket();
logger.error(`Evil: <<BAD>> Successfully created a WebSocket!`);
} catch (e) {
logger.info(`Evil: Good error on WebSocket! ${e}`);
// No need to log good stuff unless we're testing
// logger.info(`Evil: Good error on WebSocket! ${e}`);
}

try {
Expand All @@ -130,15 +269,17 @@ async function tryImports() {
`Evil: <<BAD>> Successfully dynamically imported fs! fs.readFileSync = ${fs.readFileSync}`,
);
} catch (e) {
logger.info(`Evil: Good error on dynamic import! ${e.message}`);
// No need to log good stuff unless we're testing
// logger.info(`Evil: Good error on dynamic import! ${e.message}`);
}

try {
// This should always work.
const genericFetch = await (await papi.fetch('https://www.example.com')).text();
logger.info(
/* const genericFetch = */ await (await papi.fetch('https://www.example.com')).text();
// No need to log good stuff unless we're testing
/* logger.info(
`Evil: Good success - could papi.fetch example.com "${genericFetch.substring(0, 100)}"`,
);
); */
} catch (e) {
logger.error(`Evil: <<BAD>> error on papi.fetch! ${e}`);
}
Expand All @@ -158,7 +299,14 @@ async function activate(context) {
);
papi.webViews.getWebView(evilWebViewProvider.webViewType, undefined, { existingId: '?' });

const evilFileWebViewProviderPromise = papi.webViewProviders.register(
evilFileWebViewProvider.webViewType,
evilFileWebViewProvider,
);
papi.webViews.getWebView(evilFileWebViewProvider.webViewType, undefined, { existingId: '?' });

context.registrations.add(await evilWebViewProviderPromise);
context.registrations.add(await evilFileWebViewProviderPromise);

logger.info('Evil is finished activating!');
}
Expand Down
7 changes: 7 additions & 0 deletions extensions/src/hello-world/web-views/hello-world.web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { DialogTypes } from 'renderer/components/dialogs/dialog-definition.
import type { WebViewProps } from 'shared/data/web-view.model';
import { ProjectDataTypes } from 'papi-shared-types';
import Clock from './components/clock.component';
import Logo from '../assets/offline.svg';

type Row = {
id: string;
Expand Down Expand Up @@ -171,6 +172,12 @@ globalThis.webViewComponent = function HelloWorld({
<div>
<div className="title">
Hello World <span className="framework">React</span>
{/**
* Note: `Logo` here is inlined into this code as a `data:` url. This is here simply for
* demonstration purposes. Inlining as a `data:` url is generally not recommended. Rather,
* it is generally better to use `papi-extension:` to avoid unnecessary bloat
*/}
<img width={16} height={16} src={`${Logo}`} alt="Hello World Logo" />
</div>
<div>
<Button
Expand Down
Loading