Merge pull request #708 from remy/fix/loop-protection-702

Fix loop protection
This commit is contained in:
Remy Sharp 2013-07-15 07:50:05 -07:00
commit e52af727f1
11 changed files with 301 additions and 315 deletions

View File

@ -18,7 +18,7 @@
"url": "git://github.com/remy/jsbin.git"
},
"scripts": {
"test": "./node_modules/mocha/bin/mocha"
"test": "mocha"
},
"dependencies": {
"express": "3.0.x",

View File

@ -1,202 +1,174 @@
var consoleTest = /(^.|\b)console\./;
var getPreparedCode = (function () {
var iframedelay = (function () {
var iframedelay = { active : false },
iframe = document.createElement('iframe'),
doc,
callbackName = '__callback' + (+new Date);
var consoleTest = /(^.|\b)console\./,
re = {
docReady: /\$\(document\)\.ready/,
shortDocReady: /\$\(function/,
console: /(^.|\b)console\.(\S+)/g,
script: /<\/script/ig,
code: /%code%/,
title: /<title>(.*)<\/title>/i,
winLoad: /window\.onload\s*=/,
scriptopen: /<script/gi
};
iframe.style.height = iframe.style.width = '1px';
iframe.style.visibility = 'hidden';
document.body.appendChild(iframe);
doc = iframe.contentDocument || iframe.contentWindow.document;
window[callbackName] = function (width) {
iframedelay.active = width === 0;
try {
iframe.parentNode.removeChild(iframe);
delete window[callbackName];
} catch (e){}
var two = function (i) {
return ('0'+i).slice(-2);
};
try {
doc.open();
doc.write('<script>window.parent.' + callbackName + '(window.innerWidth)</script>');
doc.close();
} catch (e) {
iframedelay.active = true;
}
return function (nojs) {
// reset all the regexp positions for reuse
re.docReady.lastIndex = 0;
re.shortDocReady.lastIndex = 0;
re.console.lastIndex = 0;
re.script.lastIndex = 0;
re.code.lastIndex = 0;
re.title.lastIndex = 0;
re.winLoad.lastIndex = 0;
re.scriptopen.lastIndex = 0;
return iframedelay;
}());
var parts = [],
source = '',
js = '',
css = '',
close = '',
hasHTML = false,
hasCSS = false,
hasJS = false,
date = new Date(),
scriptOffset = 0;
var re = null;
function two(i) {
return ('0'+i).slice(-2);
}
function getPreparedCode(nojs) {
// init the regular expression cache because this function
// is called much earlier than the above code is actually encountered
// yay for massive .js app!
if (!re) {
re = {
docReady: /\$\(document\)\.ready/,
shortDocReady: /\$\(function/,
console: /(^.|\b)console\.(\S+)/g,
script: /<\/script/ig,
code: /%code%/,
title: /<title>(.*)<\/title>/i,
winLoad: /window\.onload\s*=/,
scriptopen: /<script/gi
};
}
// reset all the regexp positions for reuse
re.docReady.lastIndex = 0;
re.shortDocReady.lastIndex = 0;
re.console.lastIndex = 0;
re.script.lastIndex = 0;
re.code.lastIndex = 0;
re.title.lastIndex = 0;
re.winLoad.lastIndex = 0;
re.scriptopen.lastIndex = 0;
var parts = [],
source = '',
js = '',
css = '',
close = '',
hasHTML = false,
hasCSS = false,
hasJS = false,
date = new Date(),
scriptOffset = 0;
try {
source = editors.html.render();
} catch (e) {
window.console && window.console.error(e.message);
}
hasHTML = !!$.trim(source);
if (!nojs) {
try {
js = editors.javascript.render();
if (js.trim()) js += '\n\n// created @ ' + two(date.getHours()) + ':' + two(date.getMinutes()) + ':' + two(date.getSeconds());
source = editors.html.render();
} catch (e) {
window.console && window.console.error(e.message);
}
}
hasJS = !!js.trim();
hasHTML = !!$.trim(source);
try {
css = editors.css.render();
} catch (e) {
window.console && window.console.error(e.message);
}
if (!nojs) {
try {
js = editors.javascript.render();
hasCSS = !!$.trim(css);
// escape any script tags in the JS code, because that'll break the mushing together
js = js.replace(re.script, '<\\/script');
// note that I'm using split and reconcat instead of replace, because if the js var
// contains '$$' it's replaced to '$' - thus breaking Prototype code. This method
// gets around the problem.
if (!hasHTML && hasJS) {
source = "<pre>\n" + js.replace(/[<>&]/g, function (m) {
if (m == '<') return '&lt;';
if (m == '>') return '&gt;';
if (m == '"') return '&quot;';
}) + "</pre>";
} else if (re.code.test(source)) {
parts = source.split('%code%');
source = parts[0] + js + parts[1];
scriptOffset = parts[0].split('\n').length;
} else if (hasJS) {
close = '';
if (source.indexOf('</body>') !== -1) {
parts.push(source.substring(0, source.lastIndexOf('</body>')));
parts.push(source.substring(source.lastIndexOf('</body>')));
source = parts[0];
close = parts.length == 2 && parts[1] ? parts[1] : '';
if (js.trim()) js += '\n\n// created @ ' + two(date.getHours()) + ':' + two(date.getMinutes()) + ':' + two(date.getSeconds());
} catch (e) {
window.console && window.console.error(e.message);
}
}
// RS: not sure why I ran this in closure, but it means the expected globals are no longer so
// js = "window.onload = function(){" + js + "\n}\n";
var type = jsbin.panels.panels.javascript.type ? ' type="text/' + jsbin.panels.panels.javascript.type + '"' : '';
hasJS = !!js.trim();
scriptOffset = source.split('\n').length - 1;
source += "<script" + type + ">\n" + js + "\n</script>\n" + close;
}
// redirect console logged to our custom log while debugging
if (re.console.test(source)) {
var replaceWith = 'window.runnerWindow.proxyconsole.';
// yes, this code looks stupid, but in fact what it does is look for
// 'console.' and then checks the position of the code. If it's inside
// an openning script tag, it'll change it to window.top._console,
// otherwise it'll leave it.
source = source.replace(re.console, function (all, str, arg, pos) {
var open = source.lastIndexOf('<script', pos),
close = source.lastIndexOf('</script', pos);
if (open > close) {
return replaceWith + arg;
} else {
return all;
}
});
}
if (!hasHTML && !hasJS && hasCSS) {
source = "<pre>\n" + css + "</pre>";
} else if (css && hasHTML) {
parts = [];
close = '';
if (source.indexOf('</head>') !== -1) {
parts.push(source.substring(0, source.lastIndexOf('</head>')));
parts.push(source.substring(source.lastIndexOf('</head>')));
source = parts[0];
close = parts.length == 2 && parts[1] ? parts[1] : '';
try {
css = editors.css.render();
} catch (e) {
window.console && window.console.error(e.message);
}
source += '<style>\n' + css + '\n</style>\n' + close;
scriptOffset += (2 + css.split('\n').length);
}
// specific change for rendering $(document).ready() because iframes doesn't trigger ready (TODO - really test in IE, may have been fixed...)
// if (re.docReady.test(source)) {
// source = source.replace(re.docReady, 'window.onload = ');
// } else if (re.shortDocReady.test(source)) {
// source = source.replace(re.shortDocReady, 'window.onload = (function');
// }
hasCSS = !!$.trim(css);
// Add defer to all inline script tags in IE.
// This is because IE runs scripts as it loads them, so variables that scripts like jQuery add to the
// global scope are undefined. See http://jsbin.com/ijapom/5
if (jsbin.ie && re.scriptopen.test(source)) {
source = source.replace(/<script(.*?)>/gi, function (all, match) {
if (match.indexOf('src') !== -1) {
return all;
} else {
return '<script defer' + match + '>';
// Rewrite loops to detect infiniteness.
// This is done by rewriting the for/while/do loops to perform a check at
// the start of each iteration.
js = loopProtect.rewriteLoops(js);
// escape any script tags in the JS code, because that'll break the mushing together
js = js.replace(re.script, '<\\/script');
// note that I'm using split and reconcat instead of replace, because if the js var
// contains '$$' it's replaced to '$' - thus breaking Prototype code. This method
// gets around the problem.
if (!hasHTML && hasJS) {
source = "<pre>\n" + js.replace(/[<>&]/g, function (m) {
if (m == '<') return '&lt;';
if (m == '>') return '&gt;';
if (m == '"') return '&quot;';
}) + "</pre>";
} else if (re.code.test(source)) {
parts = source.split('%code%');
source = parts[0] + js + parts[1];
scriptOffset = parts[0].split('\n').length;
} else if (hasJS) {
close = '';
if (source.indexOf('</body>') !== -1) {
parts.push(source.substring(0, source.lastIndexOf('</body>')));
parts.push(source.substring(source.lastIndexOf('</body>')));
source = parts[0];
close = parts.length == 2 && parts[1] ? parts[1] : '';
}
});
// RS: not sure why I ran this in closure, but it means the expected globals are no longer so
// js = "window.onload = function(){" + js + "\n}\n";
var type = jsbin.panels.panels.javascript.type ? ' type="text/' + jsbin.panels.panels.javascript.type + '"' : '';
scriptOffset = source.split('\n').length - 1;
source += "<script" + type + ">\n" + js + "\n</script>\n" + close;
}
// redirect console logged to our custom log while debugging
if (re.console.test(source)) {
var replaceWith = 'window.runnerWindow.proxyConsole.';
// yes, this code looks stupid, but in fact what it does is look for
// 'console.' and then checks the position of the code. If it's inside
// an openning script tag, it'll change it to window.top._console,
// otherwise it'll leave it.
source = source.replace(re.console, function (all, str, arg, pos) {
var open = source.lastIndexOf('<script', pos),
close = source.lastIndexOf('</script', pos);
if (open > close) {
return replaceWith + arg;
} else {
return all;
}
});
}
if (!hasHTML && !hasJS && hasCSS) {
source = "<pre>\n" + css + "</pre>";
} else if (css && hasHTML) {
parts = [];
close = '';
if (source.indexOf('</head>') !== -1) {
parts.push(source.substring(0, source.lastIndexOf('</head>')));
parts.push(source.substring(source.lastIndexOf('</head>')));
source = parts[0];
close = parts.length == 2 && parts[1] ? parts[1] : '';
}
source += '<style>\n' + css + '\n</style>\n' + close;
scriptOffset += (2 + css.split('\n').length);
}
// specific change for rendering $(document).ready() because iframes doesn't trigger ready (TODO - really test in IE, may have been fixed...)
// if (re.docReady.test(source)) {
// source = source.replace(re.docReady, 'window.onload = ');
// } else if (re.shortDocReady.test(source)) {
// source = source.replace(re.shortDocReady, 'window.onload = (function');
// }
// Add defer to all inline script tags in IE.
// This is because IE runs scripts as it loads them, so variables that
// scripts like jQuery add to the global scope are undefined.
// See http://jsbin.com/ijapom/5
if (jsbin.ie && re.scriptopen.test(source)) {
source = source.replace(/<script(.*?)>/gi, function (all, match) {
if (match.indexOf('src') !== -1) {
return all;
} else {
return '<script defer' + match + '>';
}
});
}
// read the element out of the source code and plug it in to our document.title
var newDocTitle = source.match(re.title);
if (newDocTitle !== null && newDocTitle[1] !== documentTitle) {
documentTitle = newDocTitle[1];
document.title = documentTitle + ' - ' + 'JS Bin';
}
return { source: source, scriptOffset: scriptOffset };
}
// read the element out of the source code and plug it in to our document.title
var newDocTitle = source.match(re.title);
if (newDocTitle !== null && newDocTitle[1] !== documentTitle) {
documentTitle = newDocTitle[1];
document.title = documentTitle + ' - ' + 'JS Bin';
}
return { source: source, scriptOffset: scriptOffset };
}
}());

View File

@ -0,0 +1,92 @@
/**
* Protect against infinite loops.
* Look for for, while and do loops, and insert a check function at the start of
* the loop. If the check function is called many many times then it returns
* true, preventing the loop from running again.
*/
var loopProtect = (function () {
var loopProtect = {};
// used in the loop detection
loopProtect.counters = {};
/**
* Look for for, while and do loops, and inserts *just* at the start of the
* loop, a check function.
*/
loopProtect.rewriteLoops = function (code, offset) {
var recompiled = [],
lines = code.split('\n'),
re = /for\b|while\b|do\b/;
if (!offset) offset = 0;
var method = 'window.runnerWindow.protect';
lines.forEach(function (line, i) {
var index = 0,
lineNum = i - offset;
if (re.test(line) && line.indexOf('jsbin') === -1) {
// try to insert the tracker after the openning brace (like while (true) { ^here^ )
index = line.indexOf('{');
if (index !== -1) {
line = line.substring(0, index + 1) + ';\nif (' + method + '({ line: ' + lineNum + ' })) break;';
} else {
index = line.indexOf(')');
if (index !== -1) {
// look for a one liner
var colonIndex = line.substring(index).indexOf(';');
if (colonIndex !== -1) {
// in which case, rewrite the loop to add braces
colonIndex += index;
line = line.substring(0, index + 1) + '{\nif (' + method + '({ line: ' + lineNum + ' })) break;\n' + line.substring(index + 1) + '\n}\n'; // extra new lines ensure we clear comment lines
}
}
}
line = ';' + method + '({ line: ' + lineNum + ', reset: true });\n' + line;
loopProtect.counters[lineNum] = {};
}
recompiled.push(line);
});
return recompiled.join('\n');
};
/**
* Injected code in to user's code to **try** to protect against infinite
* loops cropping up in the code, and killing the browser. Returns true
* when the loops has been running for more than 100ms.
*/
loopProtect.protect = function (state) {
loopProtect.counters[state.line] = loopProtect.counters[state.line] || {};
var line = loopProtect.counters[state.line];
if (state.reset) {
line.time = +new Date;
}
if ((+new Date - line.time) > 100) {
// We've spent over 100ms on this loop... smells infinite.
var msg = "Suspicious loop detected at line " + state.line;
if (window.proxyConsole) {
window.proxyConsole.error(msg);
} else console.error(msg);
// Returning true prevents the loop running again
return true;
}
return false;
};
loopProtect.reset = function () {
// reset the counters
loopProtect.counters = {};
};
return loopProtect;
}());
if (typeof exports !== 'undefined') {
module.exports = loopProtect;
}

View File

@ -42,78 +42,6 @@ var processor = (function () {
}) + '</pre>';
};
// used in the loop detection
processor.counters = {};
/**
* Look for for, while and do loops, and inserts *just* at the start
* of the loop, a check function. If the check function is called
* many many times, then it throws an exception suspecting this might
* be an infinite loop.
*/
processor.rewriteLoops = function (code, offset) {
var recompiled = [],
lines = code.split('\n'),
re = /for\b|while\b|do\b/;
if (!offset) offset = 0;
// reset the counters
processor.counters = {};
var counter = 'window.runnerWindow.protect';
lines.forEach(function (line, i) {
var index = 0,
lineNum = i - offset;
if (re.test(line) && line.indexOf('jsbin') === -1) {
// try to insert the tracker after the openning brace (like while (true) { ^here^ )
index = line.indexOf('{');
if (index !== -1) {
line = line.substring(0, index + 1) + ';\nif (' + counter + '({ line: ' + lineNum + ' })) break;';
} else {
index = line.indexOf(')');
if (index !== -1) {
// look for a one liner
var colonIndex = line.substring(index).indexOf(';');
if (colonIndex !== -1) {
// in which case, rewrite the loop to add braces
colonIndex += index;
line = line.substring(0, index + 1) + '{\nif (' + counter + '({ line: ' + lineNum + ' })) break;\n' + line.substring(index + 1) + '\n}\n'; // extra new lines ensure we clear comment lines
}
}
}
line = ';' + counter + '({ line: ' + lineNum + ', reset: true });\n' + line;
processor.counters[lineNum] = {};
}
recompiled.push(line);
});
return recompiled.join('\n');
};
/**
* Injected code in to user's code to **try** to protect against infinite loops
* cropping up in the code, and killing the browser. This will throw an exception
* when a loop has hit over X number of times.
*/
processor.protect = function (state) {
var line = processor.counters[state.line];
if (state.reset) {
line.count = 0;
} else {
line.count++;
if (line.count > 100000) {
// we've done a ton of loops, then let's say it smells like an infinite loop
console.error("Suspicious loop detected at line " + state.line);
return true;
}
}
return false;
};
/**
* Render build the final source code to be written to the iframe. Takes
* the original source and an options object.
@ -123,7 +51,6 @@ var processor = (function () {
options = options || [];
source = source || '';
var combinedSource = [],
realtime = (options.requested !== true),
noRealtimeJs = (options.includeJsInRealtime === false);
@ -138,13 +65,6 @@ var processor = (function () {
// the editable area.
source = source.replace(/(<.*?\s)(autofocus)/g, '$1');
// since we're running in real time, let's try hook in some loop protection
// basically if a loop runs for many, many times, it's probably an infinite loop
// so we'll throw an exception. This is done by rewriting the for/while/do
// loops to call our check at the start of each.
source = processor.rewriteLoops(source, options.scriptOffset);
// Make sure the doctype is the first thing in the source
var doctypeObj = processor.getDoctype(source),
doctype = doctypeObj.doctype;

View File

@ -3,17 +3,17 @@
* Proxy console.logs out to the parent window
* ========================================================================== */
var proxyconsole = (function () {
var proxyConsole = (function () {
var supportsConsole = true;
try { window.console.log('runner'); } catch (e) { supportsConsole = false; }
var proxyconsole = {};
var proxyConsole = {};
/**
* Stringify all of the console objects from an array for proxying
*/
proxyconsole.stringifyArgs = function (args) {
proxyConsole.stringifyArgs = function (args) {
var newArgs = [];
// TODO this was forEach but when the array is [undefined] it wouldn't
// iterate over them
@ -34,10 +34,10 @@ var proxyconsole = (function () {
var methods = ['debug', 'error', 'info', 'log', 'warn', 'dir', 'props'];
methods.forEach(function (method) {
// Create console method
proxyconsole[method] = function () {
proxyConsole[method] = function () {
// Replace args that can't be sent through postMessage
var originalArgs = [].slice.call(arguments),
args = proxyconsole.stringifyArgs(originalArgs);
args = proxyConsole.stringifyArgs(originalArgs);
// Post up with method and the arguments
runner.postMessage('console', {
method: method,
@ -51,6 +51,6 @@ var proxyconsole = (function () {
};
});
return proxyconsole;
return proxyConsole;
}());

View File

@ -84,8 +84,8 @@ var runner = (function () {
// that the user's code (that runs as a result of the following
// childDoc.write) can access the objects.
childWindow.runnerWindow = {
proxyconsole: proxyconsole,
protect: processor.protect
proxyConsole: proxyConsole,
protect: loopProtect.protect
};
// Write the source out. IE crashes if you have lots of these, so that's

View File

@ -130,7 +130,7 @@ var sandbox = (function () {
output = e.message;
type = 'error';
}
return proxyconsole[type](output);
return proxyConsole[type](output);
};
/**

View File

@ -21,6 +21,7 @@
"/js/vendor/codemirror3/addon/search/match-highlighter.js",
"/js/vendor/json2.js",
"/js/vendor/prettyprint.js",
"/js/runner/loop-protect.js",
"/js/chrome/storage.js",
"/js/jsbin.js",
"/js/editors/mobileCodeMirror.js",
@ -51,7 +52,8 @@
"/js/vendor/polyfills.js",
"/js/vendor/stringify.js",
"/js/runner/utils.js",
"/js/runner/proxyconsole.js",
"/js/runner/loop-protect.js",
"/js/runner/proxy-console.js",
"/js/runner/processor.js",
"/js/runner/sandbox.js",
"/js/runner/runner.js",

View File

@ -1,10 +1,10 @@
var assert = require('assert');
var sinon = require('sinon');
var processor = require('../public/js/runner/processor');
var loopProtect = require('../public/js/runner/loop-protect');
// expose a window object for processor compatibility
// expose a window object for loopProtect compatibility
global.window = {
runnerWindow: processor
runnerWindow: loopProtect
};
var code = {
@ -14,7 +14,7 @@ var code = {
simplewhile: 'var i = 0; while (i < 100) {\ni += 10;\n}\nreturn i;',
onelinewhile: 'var i = 0; while (i < 100) i += 10;\nreturn i;',
whiletrue: 'var i = 0;\nwhile(true) {\ni++;\n}\nreturn i;',
irl1: 'var nums = [0,1];\n var total = 8;\n for(i = 0; i <= total; i++){\n var newest = nums[i--]\n nums.push(newest);\n }\n return (nums);',
irl1: 'var nums = [0,1];\n var total = 8;\n for(var i = 0; i <= total; i++){\n var newest = nums[i--]\n nums.push(newest);\n }\n return (nums);',
irl2: 'var a = 0;\n for(var j=1;j<=2;j++){\n for(var i=1;i<=60000;i++) {\n a += 1;\n }\n }\n return a;',
};
@ -32,25 +32,25 @@ describe('loop', function () {
it('should leave none loop code alone', function () {
assert(processor.rewriteLoops(code.simple) === code.simple);
assert(loopProtect.rewriteLoops(code.simple) === code.simple);
});
it('should rewrite for loops', function () {
var compiled = processor.rewriteLoops(code.simplefor);
var compiled = loopProtect.rewriteLoops(code.simplefor);
assert(compiled !== code);
var result = run(compiled);
assert(result === 9);
});
it('should rewrite one line for loops', function () {
var compiled = processor.rewriteLoops(code.onelinefor);
var compiled = loopProtect.rewriteLoops(code.onelinefor);
assert(compiled !== code);
var result = run(compiled);
assert(result === 10);
});
it('should throw on infinite while', function () {
var compiled = processor.rewriteLoops(code.whiletrue);
var compiled = loopProtect.rewriteLoops(code.whiletrue);
try { spy(compiled); } catch (e) {}
@ -58,13 +58,13 @@ describe('loop', function () {
});
it('should throw on infinite for', function () {
var compiled = processor.rewriteLoops(code.irl1);
var compiled = loopProtect.rewriteLoops(code.irl1);
try { spy(compiled); } catch (e) {}
assert(spy.threw);
});
it('should should allow nested loops to run', function () {
var compiled = processor.rewriteLoops(code.irl2);
var compiled = loopProtect.rewriteLoops(code.irl2);
assert(run(compiled) === 120000);
});

View File

@ -1,3 +1,3 @@
--require should sinon
--require should
--reporter spec
--ui bdd

View File

@ -1,39 +1,39 @@
var spike = require('../lib/spike');
// var spike = require('../lib/spike');
describe('splike.utils.makeEvent', function () {
// describe('splike.utils.makeEvent', function () {
it('should convert some string data into a valid event', function () {
var result = spike.utils.makeEvent('example', 'hello');
result.should.equal([
'data:hello',
'event:example',
'\n'
].join('\n'));
});
// it('should convert some string data into a valid event', function () {
// var result = spike.utils.makeEvent('example', 'hello');
// result.should.equal([
// 'data:hello',
// 'event:example',
// '\n'
// ].join('\n'));
// });
it('should convert and object into a valid event', function () {
var result = spike.utils.makeEvent('example', {
a: 10,
b: 20
});
result.should.equal([
'data:{"a":10,"b":20}',
'event:example',
'\n'
].join('\n'));
});
// it('should convert and object into a valid event', function () {
// var result = spike.utils.makeEvent('example', {
// a: 10,
// b: 20
// });
// result.should.equal([
// 'data:{"a":10,"b":20}',
// 'event:example',
// '\n'
// ].join('\n'));
// });
it('should create an event even if no data is passed', function () {
var result = spike.utils.makeEvent('example');
result.should.equal([
'event:example',
'\n'
].join('\n'));
});
// it('should create an event even if no data is passed', function () {
// var result = spike.utils.makeEvent('example');
// result.should.equal([
// 'event:example',
// '\n'
// ].join('\n'));
// });
it('should return nothing if nothing is passed', function () {
var result = spike.utils.makeEvent();
result.should.equal('');
});
// it('should return nothing if nothing is passed', function () {
// var result = spike.utils.makeEvent();
// result.should.equal('');
// });
});
// });