├── .gitignore ├── test ├── callback-exported.js ├── non-jsbin.test.js ├── imported.test.js ├── callback.test.js └── loop-protect.test.js ├── .travis.yml ├── bower.json ├── package.json ├── LICENSE.md ├── lib └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | tmp 4 | dist 5 | -------------------------------------------------------------------------------- /test/callback-exported.js: -------------------------------------------------------------------------------- 1 | export default function(line) { 2 | console.log(`ok: ${line}`); 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | notifications: 4 | email: false 5 | node_js: 6 | - 8 7 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loop-protect", 3 | "main": "dist/loop-protect.min.js", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/jsbin/loop-protect", 6 | "authors": [ 7 | "Remy Sharp " 8 | ], 9 | "description": "Prevent infinite loops in dynamically eval'd JavaScript.", 10 | "moduleType": [ 11 | "globals", 12 | "node" 13 | ], 14 | "keywords": [ 15 | "loop", 16 | "jsbin" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/non-jsbin.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | const Babel = require('babel-standalone'); 3 | Babel.registerPlugin('loopProtection', require('../lib')(100)); 4 | const assert = e => console.assert(e); 5 | 6 | const loopProtect = code => 7 | Babel.transform(new Function(code).toString(), { 8 | plugins: ['loopProtection'], 9 | }).code; // eslint-disable-line no-new-func 10 | const run = code => eval(`(${code})()`); // eslint-disable-line no-eval 11 | 12 | describe('non-JS Bin use', () => { 13 | it('should catch infinite loop', () => { 14 | var code = 'var i = 0; while (true) {\ni++;\n}\nreturn "∞"'; 15 | 16 | var processed = loopProtect(code); 17 | 18 | var result = run(processed); 19 | assert(result === '∞', 'code ran and returned ' + result); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/imported.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | const Babel = require('babel-standalone'); 3 | const plugin = require('../lib'); 4 | import callback from './callback-exported'; 5 | 6 | const transform = id => code => 7 | Babel.transform(new Function(code).toString(), { 8 | plugins: [id], 9 | }).code; // eslint-disable-line no-new-func 10 | 11 | const run = code => { 12 | // console.log(code); 13 | eval(`(${code})()`); // eslint-disable-line no-eval 14 | }; 15 | 16 | describe('imported anonymous callback', () => { 17 | it('uses imported callbacks', () => { 18 | const id = 'i1'; 19 | 20 | const code = `let i = 0; 21 | while (true) { 22 | i++; 23 | }`; 24 | 25 | const spy = jest.fn(); 26 | global.console = { log: spy }; 27 | 28 | // const spy = jest.spyOn(global.console, 'log'); 29 | 30 | Babel.registerPlugin(id, plugin(100, callback)); 31 | const after = transform(id)(code); 32 | run(after); 33 | expect(spy).toHaveBeenCalledWith(`ok: 3`); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Remy Sharp", 3 | "name": "loop-protect", 4 | "description": "Prevent infinite loops in dynamically eval'd JavaScript.", 5 | "main": "dist/", 6 | "version": "2.1.6", 7 | "homepage": "https://github.com/jsbin/loop-protect", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/jsbin/loop-protect.git" 11 | }, 12 | "scripts": { 13 | "test": "jest test/*.test.js", 14 | "build": "NODE_ENV=production babel lib/ --out-dir dist --copy-files", 15 | "prepublishOnly": "npm run build" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "babel": { 21 | "presets": [ 22 | "stage-0", 23 | "env" 24 | ] 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-preset-env": "^1.6.1", 29 | "babel-preset-stage-0": "^6.24.1", 30 | "babel-runtime": "^6.26.0", 31 | "babel-standalone": "^6.26.0", 32 | "jest": "^22.0.6" 33 | }, 34 | "licenses": [ 35 | { 36 | "type": "MIT", 37 | "url": "http://jsbin.mit-license.org" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 JS Bin Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/callback.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | const Babel = require('babel-standalone'); 3 | 4 | const code = `let i = 0; while (true) { i++; }; done(i)`; 5 | 6 | let done = jest.fn(); 7 | 8 | beforeEach(() => { 9 | done = jest.fn(); 10 | }); 11 | 12 | const transform = id => code => 13 | Babel.transform(new Function(code).toString(), { 14 | plugins: [id], 15 | }).code; // eslint-disable-line no-new-func 16 | 17 | const run = code => { 18 | // console.log(code); 19 | eval(`(${code})()`); // eslint-disable-line no-eval 20 | }; 21 | 22 | test('no callback', () => { 23 | const id = 'lp1'; 24 | Babel.registerPlugin(id, require('../lib')(100)); 25 | const after = transform(id)(code); 26 | run(after); 27 | expect(done).toBeCalledWith(expect.any(Number)); 28 | }); 29 | 30 | test('anonymous callback', () => { 31 | const id = 'lp2'; 32 | Babel.registerPlugin( 33 | id, 34 | require('../lib')(100, line => done(`line: ${line}`)) 35 | ); 36 | const after = transform(id)(code); 37 | run(after); 38 | expect(done).toHaveBeenCalledWith('line: 2'); 39 | }); 40 | 41 | test('arrow function callback', () => { 42 | const id = 'lp3'; 43 | const callback = line => done(`lp3: ${line}`); 44 | 45 | Babel.registerPlugin(id, require('../lib')(100, callback)); 46 | const after = transform(id)(code); 47 | run(after); 48 | expect(done).toHaveBeenCalledWith(`${id}: 2`); 49 | }); 50 | 51 | test('named function callback', () => { 52 | const id = 'lp4'; 53 | function callback(line) { 54 | done(`lp4: ${line}`); 55 | } 56 | 57 | Babel.registerPlugin(id, require('../lib')(100, callback)); 58 | const after = transform(id)(code); 59 | run(after); 60 | expect(done).toHaveBeenCalledWith(`${id}: 2`); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const generateBefore = (t, id) => 2 | t.variableDeclaration('var', [ 3 | t.variableDeclarator( 4 | id, 5 | t.callExpression( 6 | t.memberExpression(t.identifier('Date'), t.identifier('now')), 7 | [] 8 | ) 9 | ), 10 | ]); 11 | 12 | const generateInside = ({ t, id, line, ch, timeout, extra } = {}) => { 13 | return t.ifStatement( 14 | t.binaryExpression( 15 | '>', 16 | t.binaryExpression( 17 | '-', 18 | t.callExpression( 19 | t.memberExpression(t.identifier('Date'), t.identifier('now')), 20 | [] 21 | ), 22 | id 23 | ), 24 | t.numericLiteral(timeout) 25 | ), 26 | extra 27 | ? t.blockStatement([ 28 | t.expressionStatement( 29 | t.callExpression(extra, [ 30 | t.numericLiteral(line), 31 | t.numericLiteral(ch), 32 | ]) 33 | ), 34 | t.breakStatement(), 35 | ]) 36 | : t.breakStatement() 37 | ); 38 | }; 39 | 40 | const protect = (t, timeout, extra) => path => { 41 | if (!path.node.loc) { 42 | // I don't really know _how_ we get into this state 43 | // but https://jsbin.com/mipesawapi/1/ triggers it 44 | // and the node, I'm guessing after translation, 45 | // doesn't have a line in the code, so this blows up. 46 | return; 47 | } 48 | const id = path.scope.generateUidIdentifier('LP'); 49 | const before = generateBefore(t, id); 50 | const inside = generateInside({ 51 | t, 52 | id, 53 | line: path.node.loc.start.line, 54 | ch: path.node.loc.start.column, 55 | timeout, 56 | extra, 57 | }); 58 | const body = path.get('body'); 59 | 60 | // if we have an expression statement, convert it to a block 61 | if (!t.isBlockStatement(body)) { 62 | body.replaceWith(t.blockStatement([body.node])); 63 | } 64 | path.insertBefore(before); 65 | body.unshiftContainer('body', inside); 66 | }; 67 | 68 | module.exports = (timeout = 100, extra = null) => { 69 | if (typeof extra === 'string') { 70 | const string = extra; 71 | extra = `() => console.error("${string.replace(/"/g, '\\"')}")`; 72 | } else if (extra !== null) { 73 | extra = extra.toString(); 74 | if (extra.startsWith('function (')) { 75 | // fix anonymous functions as they'll cause 76 | // the callback transform to blow up 77 | extra = extra.replace(/^function \(/, 'function callback('); 78 | } 79 | } 80 | 81 | return ({ types: t, transform }) => { 82 | const node = extra ? 83 | transform(extra, { ast: true }).ast.program.body[0] : null; 84 | 85 | let callback = null; 86 | if (t.isExpressionStatement(node)) { 87 | callback = node.expression; 88 | } else if (t.isFunctionDeclaration(node)) { 89 | callback = t.functionExpression(null, node.params, node.body); 90 | } 91 | 92 | return { 93 | visitor: { 94 | WhileStatement: protect(t, timeout, callback), 95 | ForStatement: protect(t, timeout, callback), 96 | DoWhileStatement: protect(t, timeout, callback), 97 | }, 98 | }; 99 | }; 100 | }; 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test status](https://api.travis-ci.org/jsbin/loop-protect.svg?branch=master)](https://travis-ci.org/jsbin/loop-protect) 2 | 3 | # loop-protect 4 | 5 | JS Bin's loop protection implementation as a reusable library. 6 | 7 | This code protects use cases where user code includes an infinite loop using a `while`, `for` or `do` loop. 8 | 9 | Note that this does *not* solve the [halting problem](http://en.wikipedia.org/wiki/Halting_problem) but simply rewrites JavaScript (using Babel's AST) wrapping loops with a conditional break. This also *does not* protect against recursive loops. 10 | 11 | ## Example 12 | 13 | With loop protection in place, it means that a user can enter the code as follows on JS Bin, and the final `console.log` will still work. 14 | 15 | The code is transformed from this: 16 | 17 | ```js 18 | while (true) { 19 | doSomething(); 20 | } 21 | 22 | console.log('All finished'); 23 | ``` 24 | 25 | …to this: 26 | 27 | ```js 28 | let i = 0; 29 | var _LP = Date.now(); 30 | while (true) { 31 | if (Date.now() - _LP > 100) 32 | break; 33 | 34 | doSomething(); 35 | } 36 | 37 | console.log('All finished'); 38 | ``` 39 | 40 | ## Usage 41 | 42 | The loop protection is a babel transform, so can be used on the server or in the client. 43 | 44 | The previous implementation used an injected library to handle tracking loops - this version does not. 45 | 46 | ### Example (client) implementation 47 | 48 | ```js 49 | import Babel from 'babel-standalone'; 50 | import protect from 'loop-protect'; 51 | 52 | const timeout = 100; // defaults to 100ms 53 | Babel.registerPlugin('loopProtection', protect(timeout)); 54 | 55 | const transform = source => Babel.transform(source, { 56 | plugins: ['loopProtection'], 57 | }).code; 58 | 59 | // rewrite the user's JavaScript to protect loops 60 | var processed = transform(getUserCode()); 61 | 62 | // run in an iframe, and expose the loopProtect variable under a new name 63 | var iframe = getNewFrame(); 64 | 65 | // append the iframe to allow our code to run as soon as .close is called 66 | document.body.appendChild(iframe); 67 | 68 | // open the iframe and write the code to it 69 | var win = iframe.contentWindow; 70 | var doc = win.document; 71 | doc.open(); 72 | 73 | doc.write('