Published
Edited
Jul 23, 2019
1 star
Insert cell
Insert cell
class Sandbox {
constructor() {
if (!(this instanceof Sandbox))
return new Sandbox()

const iframe = this.iframe = document.createElement('iframe')

document.body.appendChild(iframe)

const SandboxedFunction = iframe.contentWindow.constructor.constructor,
[
SandboxedAsyncGeneratorFunction,
SandboxedAsyncFunction,
SandboxedGeneratorFunction,
] = new SandboxedFunction(`return [
(async function*() {}).constructor,
(async function () {}).constructor,
( function*() {}).constructor,
]`)()
document.body.removeChild(iframe)

this.SandboxedFunction = SandboxedFunction
this.SandboxedAsyncGeneratorFunction = SandboxedAsyncGeneratorFunction
this.SandboxedAsyncFunction = SandboxedAsyncFunction
this.SandboxedGeneratorFunction = SandboxedGeneratorFunction

this.wrap = new SandboxedFunction(`
return function wrap(value) {
switch (typeof value) {
case 'bigint':
case 'boolean':
case 'number':
case 'string':
case 'symbol':
case 'undefined':
// Primitives are copied by the JS runtime, and therefore cannot "leak"
return value

case 'function':
// We return a wrapper around the function, which means that accessing
// 'v.prototype.constructor' will return a sandboxed Function, rather
// than an unsafe Function.
return (...args) => wrap(v.apply(this, args))

case 'object':
// We return another safe wrapper around the object
return new Proxy(value, {
get(target, p) {
return wrap(target[p])
}
})
}
}
`)()
Object.freeze(this)
}
run(code, context, SandboxedFunction = this.SandboxedFunction) {
if (typeof code !== 'string')
throw new Error('Code must be a string.')
if (context !== undefined && (context === null || typeof context !== 'object'))
throw new Error('Context must be an object.')

if (SandboxedFunction !== this.SandboxedFunction &&
SandboxedFunction !== this.SandboxedAsyncGeneratorFunction &&
SandboxedFunction !== this.SandboxedAsyncFunction &&
SandboxedFunction !== this.SandboxedGeneratorFunction)
throw new Error('Invalid sandboxed Function constructor provided.')

const argumentNames = [],
argumentValues = []

for (const key in context || {}) {
argumentNames.push(key)
argumentValues.push(context[key])
}

return new SandboxedFunction(...argumentNames, code)(...argumentValues)
}
runAsync(code, context) {
return this.run(code, context, this.SandboxedAsyncFunction)
}
runGenerator(code, context) {
return this.run(code, context, this.SandboxedGeneratorFunction)
}
runAsyncGenerator(code, context) {
return this.run(code, context, this.SandboxedAsyncGeneratorFunction)
}
}
Insert cell
Insert cell
Insert cell
sandbox.run('return 42')
Insert cell
sandbox.run('return document.querySelectorAll(".observablehq")')
Insert cell
document.querySelectorAll('.observablehq')
Insert cell
Insert cell
html`<span id=leaked>Heh, I'm in danger!`
Insert cell
function tryLeak(person) {
sandbox.run(`
const Function = person.constructor.constructor
const replace = new Function(\`
document.getElementById("leaked").innerHTML =
document.getElementById("leaked").innerHTML
.replace(/ (is|are) in danger/, ", \${person.name} are in danger")
.replace(" I'm", " \${person.name} is")
\`)

replace()
`, { person })
}
Insert cell
tryLeak( { name: 'Bob', age: 42 } )
Insert cell
tryLeak(sandbox.wrap({ name: 'Pak', age: 56 }))
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more