Suppose we allow users to write some code and run the code in browser, how to avoid infinite loops? 🤔 Since it is a common mistake.
We need to use iframe for security concerns.
We need to sandbox the environment to avoid breaking the whole apps.
Blob url works but blob urls have the same origin as current web page, meaning the code copied from somewhere else actually have the access to the APIs, which is not good.
So data URIs is the best approach, we could use sandbox
to disable features like popups .etc, which is great.
iframes and parent windows share the same thread.
The infinite loop in the iframe causes the whole window to freeze, because it is on the same thread with parent.
So there is no way to detect timeout on parent window, since it will never be called if infinite loop happens.
Web Worker doesn't have DOM API
For pure JavaScript functions without DOM API usage, worker might be a good choice to do timeout detecting.
But for our case, we need DOM API, so Web Worker is not an option.
Final approach, we need to modify users' code
Most common infinite loops are caused by while/for
, if we inject some detecting code into the loop body, we actually could throw some errors when too many loop calls are detected.
Like code below
function abc() {
while (true) {
console.log(Date.now())
}
}
We could try to inject some code at the beginning and at the while loop body.
let __count = 0
const __detectInfiniteLoop = () => {
if (__count > 10000) {
throw new Error('Infinite Loop detected')
}
__count += 1
}
function abc() {
while (true) {
console.log(Date.now())
__detectInfiniteLoop()
}
}
generate new code with AST traversing
We could use following 3 packages.
- @babel/parser - to parse user input code to AST
- @babel/traverse - to search for/while loop
- @babel/generator - to generate new code after the injection.
First, we parse the user's code
import { parse } from '@babel/parser'
const ast = parse(code)
Then we prepare the code to be injected.
The code has 2 parts, 1st part could be inserted at the head, so string is good enough, the second parts needs to be inserted into the AST, so we parse it as well as the users' code.
const prefix = `
let __count = 0
const __detectInfiniteLoop = () => {
if (__count > 10000) {
throw new Error('Infinite Loop detected')
}
__count += 1
}
`
const detector = parse(`__detectInfiniteLoop()`)
Then we just look for for/while loops and insert the detector above.
import traverse from '@babel/traverse'
traverse(ast, {
ForStatement: function (path) {
path.node.body.body.push(...detector.program.body)
},
WhileStatement: function (path) {
path.node.body.body.push(...detector.program.body)
},
DoWhileStatement: function (path) {
path.node.body.body.push(...detector.program.body)
}
})
Now the code is modified, last step is transform it into string and run in iframe.
import generate from '@babel/generator'
const newCode = prefix + generate(ast).code
This doesn't cover all the cases
Above approach just cover the common infinite loops from while/for, and checks the loop count, which is good enough for us.
But we know it is not perfect, please tell us if you have better ideas.
Hope it helps, see you next time.
Top comments (0)