Game.prototype.verbotenWords = [ '.call', 'call(', 'apply', 'bind', // prevents arbitrary code execution 'prototype', // prevents messing with prototypes 'debugger', // prevents pausing execution 'delete', // prevents removing items 'constructor', // prevents retrieval of Function using an instance of it 'window', // prevents setting "window.[...] = map", etc. 'top', // prevents user code from escaping the iframe 'validate', 'onExit', 'objective', // don't let players rewrite these methods '\\u' // prevents usage of arbitrary code through unicode escape characters, see issue #378 ]; Game.prototype.allowedTime = 2000; // for infinite loop prevention var DummyDisplay = function () { this.clear = function () {}; this.drawAll = function () {}; this.drawObject = function () {}; this.drawText = function () {}; this.writeStatus = function () {}; }; Game.prototype.validate = function(allCode, playerCode, restartingLevelFromScript) { var game = this; try { for (var i = 0; i < this.verbotenWords.length; i++) { var badWord = this.verbotenWords[i]; if (playerCode.indexOf(badWord) > -1) { throw "You are not allowed to use '" + badWord + "'!"; } } var dummyMap = new Map(new DummyDisplay(), this); dummyMap._dummy = true; dummyMap._setProperties(this.editor.getProperties().mapProperties); // modify the code to always check time to prevent infinite loops allCode = allCode.replace(/\)\s*{/g, ") {"); // converts Allman indentation -> K&R allCode = allCode.replace(/\n\s*while\s*\((.*)\)/g, "\nfor (dummy=0;$1;)"); // while -> for allCode = $.map(allCode.split('\n'), function (line, i) { return line.replace(/for\s*\((.*);(.*);(.*)\)\s*{/g, "for ($1, startTime = Date.now();$2;$3){" + "if (Date.now() - startTime > " + game.allowedTime + ") {" + "throw '[Line " + (i+1) + "] TimeOutException: Maximum loop execution time of " + game.allowedTime + " ms exceeded.';" + "}"); }).join('\n'); allCode = "'use strict';var validateLevel,onExit,objective\n"+allCode; allCode = allCode+"\n({startLevel:startLevel,validateLevel:validateLevel,onExit:onExit,objective:objective})"; if (this._debugMode) { console.log(allCode); } var allowjQuery = dummyMap._properties.showDummyDom; // setup iframe in which code is run. As a side effect, this sets `this._eval` // and `this.SyntaxError` correctly. var userEnv = this.initIframe(allowjQuery); // evaluate the code to get startLevel() and (opt) validateLevel() methods var userOutput = this._eval(allCode); // start the level on a dummy map to validate this._setPlayerCodeRunning(true); userOutput.startLevel(dummyMap); this._setPlayerCodeRunning(false); // re-run to check if the player messed with startLevel this._startOfStartLevelReached = false; this._endOfStartLevelReached = false; dummyMap._reset(); this._setPlayerCodeRunning(true); userOutput.startLevel(dummyMap); this._setPlayerCodeRunning(false); // does startLevel() execute fully? // (if we're restarting a level after editing a script, we can't test for this // - nor do we care) if (!this._startOfStartLevelReached && !restartingLevelFromScript) { throw 'startLevel() has been tampered with!'; } if (!this._endOfStartLevelReached && !restartingLevelFromScript) { throw 'startLevel() returned prematurely!'; } this.validateLevel = function () { return true; }; // does validateLevel() succeed? if (typeof(userOutput.validateLevel) === "function") { this.validateLevel = userOutput.validateLevel; this._setPlayerCodeRunning(true); userOutput.validateLevel(dummyMap); this._setPlayerCodeRunning(false); } dummyMap._clearIntervals(); this.onExit = function () { return true; }; if (typeof userOutput.onExit === "function") { this.onExit = userOutput.onExit; } this.objective = function () { return false; }; if (typeof userOutput.objective === "function") { this.objective = userOutput.objective; } return userOutput.startLevel; } catch (e) { // cleanup this._setPlayerCodeRunning(false); if (dummyMap) { dummyMap._clearIntervals(); } var exceptionText = e.toString(); if (e instanceof this.SyntaxError) { var lineNum = this.findSyntaxError(allCode, e.message); if (lineNum) { exceptionText = "[Line " + lineNum + "] " + exceptionText; } } this.display.appendError(exceptionText); // throw e; // for debugging return null; } }; // makes sure nothing un-kosher happens during a callback within the game // e.g. item collison; function phone Game.prototype.validateCallback = function(callback, throwExceptions) { var savedException = null; var exceptionFound = false; try { // run the callback and check for forbidden method calls try { this._setPlayerCodeRunning(true); var result = callback(); this._setPlayerCodeRunning(false); } catch (e) { // cleanup this._setPlayerCodeRunning(false); if (e.toString().indexOf("Forbidden method call") > -1 || e.toString().indexOf("Attempt to modify private property") > -1 || e.toString().indexOf("Attempt to read private property") > -1) { // display error, disable player movement this.display.appendError(e.toString(), "%c{red}Please reload the level."); this.sound.playSound('static'); this.map.getPlayer()._canMove = false; this.map._callbackValidationFailed = true; this.map._clearIntervals(); // throw e; // for debugging return; } else { // other exceptions are fine here, but be sure to run validation before passing them up savedException = e; exceptionFound = true; } } // check if validator still passes try { if (typeof(this.validateLevel) === 'function') { this._setPlayerCodeRunning(true); this.validateLevel(this.map); this._setPlayerCodeRunning(false); } } catch (e) { this._setPlayerCodeRunning(false); // validation failed - not much to do here but restart the level, unfortunately this.display.appendError(e.toString(), "%c{red}Validation failed! Please reload the level."); // play error sound this.sound.playSound('static'); // disable player movement this.map.getPlayer()._canMove = false; this.map._callbackValidationFailed = true; this.map._clearIntervals(); return; } // refresh the map (unless it refreshes automatically), just in case if(!this.map._properties.refreshRate) { this.map.refresh(); } if(exceptionFound) { throw savedException; } return result; } catch (e) { this.map.writeStatus(e.toString()); // throw e; // for debugging if (throwExceptions) { throw e; } } }; Game.prototype.validateAndRunScript = function (code) { try { // Game.prototype.blah => game.blah code = code.replace(/Game.prototype/, 'this'); // Blah => game._blahPrototype code = code.replace(/function Map/, 'this._mapPrototype = function'); code = code.replace(/function Player/, 'this._playerPrototype = function'); new Function(code).bind(this).call(); // bind the function to current instance of game! if (this._mapPrototype) { // re-initialize map if necessary this.map._reset(); // for cleanup this.map = new this._mapPrototype(this.display, this); } // re-initialize objects if necessary this.objects = this.getListOfObjects(); // and restart current level from saved state var savedState = this.editor.getGoodState(this._currentLevel); this._evalLevelCode(savedState['code'], savedState['playerCode'], false, true); } catch (e) { this.display.writeStatus(e.toString()); //throw e; // for debugging } } var allowedGlobals = { // These four are allowed primarily to avoid confusing the programmer 'Object':true, 'Array':true, 'String':true, 'Number':true, // Math.Floor and Math.random are used in many levels 'Math':true, // parseInt is used in a few bonus levels 'parseInt':true, // Date is used by the infinite loop prevention code 'Date':true } Game.prototype.initIframe = function(allowjQuery){ var iframe = $("#user_code")[0]; // reset any state in the iframe iframe.src = "about:blank"; var iframewindow = iframe.contentWindow; if (iframewindow.eval) { this._eval = iframewindow.eval; this.SyntaxError = iframewindow.SyntaxError; } // delete any unwated global variables in the iframe function purgeObject(object) { var globals = Object.getOwnPropertyNames(object); for (var i = 0;i < globals.length;i++) { var variable = globals[i]; if (!allowedGlobals.hasOwnProperty(variable)) { delete object[variable]; } } var prototype = Object.getPrototypeOf(object); if (prototype && prototype != iframewindow.Object.prototype) { purgeObject(prototype); } } purgeObject(iframewindow); // document can't be deleted, so purge it instead purgeObject(iframewindow.document); // add in any necessary global variables iframewindow.ROT = {Map: {DividedMaze: ROT.Map.DividedMaze }} if (allowjQuery) { // this is not secure, however it doesn't matter since the only level // with showDummyDom set has no editable code iframewindow.$ = iframewindow.jQuery = jQuery; } return iframewindow; } // Object security // takes an object and modifies it so that all properties starting with `_` // throw an error when accessed in level code, // and that all functions are unwritable Game.prototype.secureObject = function(object, objecttype) { for (var prop in object) { if(prop == "_startOfStartLevelReached" || prop == "_endOfStartLevelReached"){ // despite starting with an _, these two properties are intended to be called from map code continue; } if(prop[0] == "_"){ this.secureProperty(object, prop, objecttype); } else if (typeof object[prop] == "function") { Object.defineProperty(object, prop, { configurable:false, writable:false }); } } } Game.prototype.secureProperty = function(object, prop, objecttype){ var val = object[prop]; var game = this; Object.defineProperty(object, prop, { configurable:false, enumerable:false, get:function(){ if (game._isPlayerCodeRunning()) { throw "Attempt to read private property " + objecttype + "." + prop; } return val; }, set:function(newValue){ if(game._isPlayerCodeRunning()) { throw "Attempt to modify private property " + objecttype + "." + prop; } val = newValue } }); } // awful awful awful method that tries to find the line // of code where a given error occurs Game.prototype.findSyntaxError = function(code, errorMsg) { var lines = code.split('\n'); // One line at the top is the added declarations and doesn't // correspond to any real editor code var phantomLines = 1; for (var i = 1; i <= lines.length; i++) { var line = lines[i - 1]; var startStartLevel = "map._startOfStartLevelReached()"; var endStartLevel = "map._endOfStartLevelReached()"; if (line == startStartLevel || line == endStartLevel ) { // This line was added by the editor and doesn't show up to the user // so shouldn't be counted. phantomLines += 1; } var testCode = lines.slice(0, i).join('\n'); try { this._eval("'use strict';" + testCode); } catch (e) { if (e.message === errorMsg) { return i - phantomLines; } } } return null; };