I'm working on an IDE that should run a user's JavaScript in the Browser. I don't want to just eval
arbitrary code, especially as code may be shared between users. From here and other sources, I learned iframes to be the likely tool for this.
On the other hand, things like Caja are also suggested, and I haven't found any example code particularly on how to use iframes for this purpose. This makes me feel too uneasy to just accept what I have come up with, without any feedback.
To reiterate, I want to:
- let users write small JavaScript programs that interact with the "world" over a small set of allowed APIs,
- execute them in their browser,
- make this secure for users who try code they didn't write themselves.
Here's what I have come up with: index.html
can launch a sandboxed iframe for src child.html
. The iframe is sent the user's code, which is then eval
'd and invokes some APIs (dispatched via postMessage
). (the origin checks are missing and targetOrigin
is '*'
because I'm testing on file://
.)
index.html
<html>
<head>
</head>
<body>
<div id="mountpoint">
</div>
<script type="text/javascript">
const handlers = {
// print program output to the console
print: function print(source, text) {
console.log(event.data.payload);
},
// clean up an iframe after execution
exit: function exit(source) {
const i = frames.findIndex(function(item) {
return item[0] == source;
});
frames[i][1].remove();
delete frames[i];
},
};
// dispatch commands from the iframe
window.addEventListener('message', function receiveMessage(event) {
// if (event.origin !== "http://example.com")
// return;
const handler = handlers[event.data.command];
if(handler) {
handler(event.source, event.data.payload);
}
}, false);
frames = [];
// create a new iframe and send it code
function execute(code) {
let frame = document.createElement('iframe');
frame.setAttribute('src', 'child.html');
frame.setAttribute('sandbox', 'allow-scripts');
frame.style.display = 'none';
document.getElementById('mountpoint').appendChild(frame);
frames.push([frame.contentWindow, frame]);
function sendMessage(command, payload) {
frame.contentWindow.postMessage({
command: command,
payload: payload,
}, '*');
}
// wait for the iframe to load before sending `execute`
setTimeout(function() {
sendMessage('execute', code);
}, 0);
}
execute(`
sendMessage('print', 'this is the child!!');
`);
</script>
</body>
</html>
child.html
<html>
<head>
</head>
<body>
<script type="text/javascript">
const handlers = {
// execute the given program
execute: function execute(source, code) {
function sendMessage(command, payload) {
event.source.postMessage({
command: command,
payload: payload,
}, '*');
}
try {
// run the code
eval(code);
} finally {
// ask parent to clean up
sendMessage('exit');
}
},
};
// dispatch commands from the parent
window.addEventListener('message', function receiveMessage(event) {
// if (event.origin !== "http://example.com")
// return;
const handler = handlers[event.data.command];
if(handler) {
handler(event.source, event.data.payload);
}
}, false);
</script>
</body>
</html>
Is this a proper sandboxing approach? Are there ways to escape the sandbox with what I've written here? As a bonus, what better results could I get from using Caja, or other libraries that get mentioned with respect to this topic?