diff options
author | Nik Nyby <nikolas@gnu.org> | 2015-01-17 17:12:36 -0500 |
---|---|---|
committer | Nik Nyby <nikolas@gnu.org> | 2015-01-17 17:12:36 -0500 |
commit | ada88090ead2c3b9d0804794c5f20f9b24d1c2b1 (patch) | |
tree | 2838a7eee6c5d74094216acebd86915e0ea1de42 /lib/js_checker/nontrivial_checker.js | |
download | librejsxul-ada88090ead2c3b9d0804794c5f20f9b24d1c2b1.tar.lz librejsxul-ada88090ead2c3b9d0804794c5f20f9b24d1c2b1.tar.xz librejsxul-ada88090ead2c3b9d0804794c5f20f9b24d1c2b1.zip |
Import to new git repository
The old repository was using almost 100mb of space because of all
the unnecessary files in the history. So I've imported the code to a
new git repository. Unfortunately the history isn't viewable from this
repository anymore. To see what happened with LibreJS before 2015, see
the old Bazaar repo here: http://bzr.savannah.gnu.org/lh/librejs/
Diffstat (limited to 'lib/js_checker/nontrivial_checker.js')
-rw-r--r-- | lib/js_checker/nontrivial_checker.js | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/lib/js_checker/nontrivial_checker.js b/lib/js_checker/nontrivial_checker.js new file mode 100644 index 0000000..4d3bc8b --- /dev/null +++ b/lib/js_checker/nontrivial_checker.js @@ -0,0 +1,374 @@ +/** + * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. + * * + * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +const types = require("js_checker/constant_types"); + +// constants from Narcissus for function types. +const DECLARED_FORM = 0, EXPRESSED_FORM = 1, STATEMENT_FORM = 2; + +const token = types.token; + +var checkTypes = types.checkTypes; + +var utils = { + /** + * nodeContains + * Checks that node contains both a type and a value. + * Shortcut to check for null/undefined. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if matching. + */ + nodeContains: function (n, type, value) { + if (n != undefined) { + return n.type === type && + n.value === value; + } + }, + + /** + * isType + * Checks that node is of a certain type. + * Shortcut to check for null/undefined. + * + * @param {object} n. The current node being studied. + * @return {boolean}. True if it's the right type. + */ + isType: function (n, type) { + return n != undefined && n.type === type; + }, + + isNotType: function (n, type) { + return n != undefined && n.type !== type; + }, + + /** + * hasChildren + * + * Checks the token on the left + * and on the right. + * + * @param {object} n. The current node being studied. + * @param {leftType} token constant. The type on the + * left. + * @param {rightType} token constant. The type of child + * on the right + * + */ + hasChildren: function (n, leftType, rightType) { + if (types == undefined) { + return false; + } + return this.isType(n.children[0], leftType) && + this.isType(n.children[1], rightType); + }, + + /** + * findScriptTag + * + * This method should probably be replaced with DOM testing + * as regex is rather insufficiant, and this wouldn't cover + * tricky constructs as shown in http://ha.ckers.org/xss.html. + */ + findScriptTag: function (n) { + return n.value != undefined && + /<script[^>]*?>/i.test(n.value); + } +}; + +var NonTrivialChecker = function() { + this.definesFunction = false; + this.hash = null; +}; + +/** + * definesFunctionFound + * + * Returns true if it finds a node of type FUNCTION + * that isn't a callback or an IIFE. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + */ +NonTrivialChecker.prototype.definesFunctionFound = function (n) { + var isFunction = false; + if (n.type === token.FUNCTION && + n.body != undefined) { + + if (n.functionForm !== token.DECLARED_FORM && + ((n.parent.type === token.LIST && + n.parent.parent.type === token.CALL) || + n.parent.type === token.CALL) && + n.name == undefined) { + // this is a callback or an immediately + // invoked function expression "IIFE". + isFunction = false; + } else { + // this is a regular function declaration or + // function expression assigned to a variable. + //console.log("THIS DEFINES FUNCTION"); + isFunction = true; + } + } + + // look for Function constructor. + if (n.type === token.IDENTIFIER && + n.value === 'Function' && + (n.parent.type === token.NEW_WITH_ARGS || + n.parent.type === token.CALL)) { + // this is a Function constructor. + //console.log("THIS DEFINES FUNCTION"); + isFunction = true; + } + + return isFunction; +}; + + +/** + * invokesEval + * + * Returns true (nontrivial) if it finds any use of + * the eval function. For simplicity, we assume any + * use of an identifier "eval" is the eval function. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + */ +NonTrivialChecker.prototype.invokesEval = function (n) { + return (n.type === token.CALL && + utils.nodeContains(n.children[0], token.IDENTIFIER, 'eval') || + n.type === token.IDENTIFIER && n.value === 'eval'); +}; + +/** + * evalIdentifier + * + * Returns true (nontrivial) if it finds any use of + * the eval function. For simplicity, we assume any + * use of an identifier "eval" is the eval function. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + */ +NonTrivialChecker.prototype.evalIdentifier = function (n) { + return n.type === token.IDENTIFIER && n.value === 'eval'; +}; + + +/** + * invokesMethodBracketSuffix + * + * Finds a method being invoked using the bracket suffix notation + * rather than the dot notation. It is difficult without keeping track of + * variable values to check for what method is actually being called. + * So we're just flagging any use of this construct as nontrivial. + * e.g., should catch: xhr[a+b]('GET', 'http://www.example.com'); + * Should not catch other uses such as: myArray[num]; + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + */ +NonTrivialChecker.prototype.invokesMethodBracketSuffix = function (n) { + return n.type === token.CALL && utils.isType(n.children[0], token.INDEX); +}; + +/** + * createsXhrObject + * + * Creates an xhr object. + * Since all "new XMLHttpRequest", "XMLHttpRequest()", + * and "new window.XMLHttpRequest" instantiate the xhr object, + * we assume (without further proof) that any use + * of the identifier "XMLHttpRequest" and "ActiveXObject" + * is an xhr object. + * Constructs like window[a+b]() are already caught by the + * bracket suffix check. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + */ +NonTrivialChecker.prototype.createsXhrObject = function (n) { + return (n.type === token.IDENTIFIER) && + (n.value === 'XMLHttpRequest' || + n.value === 'ActiveXObject'); +}; + +/** + * invokesXhrOpen + * + * Here we assume the call of an open method must be an xhr request + * (and not some other object) by checking the number of arguments. + * In most cases this method won't be used since createsXhrObject + * will already have caught the xhr. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + * + */ +NonTrivialChecker.prototype.invokesXhrOpen = function (n) { + return n.type === token.CALL && + utils.hasChildren(n, token.DOT, token.LIST) && + utils.isType(n.children[0].children[0], token.IDENTIFIER) && + utils.nodeContains(n.children[0].children[1], token.IDENTIFIER, 'open') && + n.children[1].children.length > 1; +}; + +/** + * createsScriptElement + * + * Checks for document.createElement() that create a script. In the case + * it creates an element from a variable, we assume it's a script. In the + * future we might want to check for the value of that string variable + * (e.g., if a variable is assigned 'script', raise a flag) + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + * + * + */ +NonTrivialChecker.prototype.createsScriptElement = function (n) { + return n.type === token.CALL && + utils.hasChildren(n, token.DOT, token.LIST) && + utils.isType(n.children[0].children[0], token.IDENTIFIER) && + utils.nodeContains(n.children[0].children[1], token.IDENTIFIER, 'createElement') && + (utils.nodeContains(n.children[1].children[0], token.STRING, 'script') || + utils.isType(n.children[1].children[0], token.IDENTIFIER)); +}; + +/** + * writesScriptAsHtmlString + * + * catches myObj.write('<script></script>'); + * or any myObj.write(myStringVariable); + * or concatenation such as: + * myObj.write('<scri' + stringVariable); + * or 'something' + 'somethingelse'. + * + * To check for javascript here we might want to look at the list + * from ha.ckers.org/xss.html for the future. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + * + */ +NonTrivialChecker.prototype.writesScriptAsHtmlString = function (n) { + var listArg; + + if (n.type === token.CALL && + utils.hasChildren(n, token.DOT, token.LIST) && + utils.isType(n.children[0].children[0], token.IDENTIFIER) && + utils.nodeContains(n.children[0].children[1], token.IDENTIFIER, 'write') + ) { + if (utils.isNotType(n.children[1].children[0], token.STRING)) { + // return true if any operation or concatenation. + // We are cautious (as it could + // embed a script) and flag this as nontrivial. + + return true; + } + return utils.findScriptTag(n.children[1].children[0]); + } else { + return false; + } +}; + +/** + * nontrivial anytime we see an identifier as innerHTML + */ +NonTrivialChecker.prototype.innerHTMLIdentifier = function (n) { + if ((n.type === token.IDENTIFIER || + n.type === token.STRING) && + n.value === 'innerHTML' + ) { + return true; + } +}; + +/** + * checkNontrivial + * + * Contains all the conditionals that try to identify, + * step by step, all code that could be flagged as + * nontrivial. + * + * @param {object} n. The current node being studied. + * @return {boolean} . True if found. + * + */ +NonTrivialChecker.prototype.checkNontrivial = function (n, t) { + + if (n.type === token.IDENTIFIER && this.evalIdentifier(n)) { + //console.log("NONTRIVIAL: eval has been found in code"); + return types.nontrivialWithComment("NONTRIVIAL: eval has been found in code"); + } + + if (this.innerHTMLIdentifier(n)) { + //console.log("NONTRIVIAL: innerHTML identifier"); + return types.nontrivialWithComment("NONTRIVIAL: innerHTML identifier"); + } + + // the node is an identifier + if (n.type === token.IDENTIFIER && this.createsXhrObject(n)) { + //console.log('NONTRIVIAL: Creates an xhr object'); + return types.nontrivialWithComment('NONTRIVIAL: Creates an xhr object'); + } + + // this is a method/function call + if (n.type === token.CALL) { + + if (this.invokesEval(n)) { + //console.log("NONTRIVIAL: eval has been found in code"); + return types.nontrivialWithComment("NONTRIVIAL: eval has been found in code"); + } + + if (this.invokesMethodBracketSuffix(n)) { + //console.log('NONTRIVIAL: square bracket suffix method call detected'); + return types.nontrivialWithComment("NONTRIVIAL: eval has been found in code"); + } + + if (this.invokesXhrOpen(n)) { + //console.log('NONTRIVIAL: an open method similar to xhr.open is used'); + return types.nontrivialWithComment('NONTRIVIAL: square bracket suffix method call detected'); + } + + if (this.createsScriptElement(n)) { + //console.log('NONTRIVIAL: creates script element dynamically.'); + return types.nontrivialWithComment('NONTRIVIAL: an open method similar to xhr.open is used'); + } + + if (this.writesScriptAsHtmlString(n)) { + //console.log('NONTRIVIAL: writes script as html dynamically.'); + return types.nontrivialWithComment('NONTRIVIAL: creates script element dynamically.'); + } + } + + // The node is a function definition. + // Most common occurence. + if (this.definesFunctionFound(n)) { + return types.trivialFuncWithComment("Script is trivial but defines one or more functions"); + } + + // found nothing else, so trivial. + return types.trivialWithComment("Script is trivial"); +}; + +exports.nonTrivialChecker = function () { + return new NonTrivialChecker(); +}; |