/** * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. * * * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros * Copyright (C) 2014, 2015 Nik Nyby * * This file is part of GNU LibreJS. * * GNU LibreJS 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. * * GNU LibreJS 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 GNU LibreJS. If not, see . */ var scriptProperties = require("./script_properties"); const scriptTypes = scriptProperties.scriptTypes; const scriptsCached = require("../../script_entries/scripts_cache") .scriptsCached; const statusTypes = scriptProperties.statusTypes; // object model for script entries. var scriptObject = require("./script_object"); var urlHandler = require("../../url_handler/url_handler"); var attributeHelpers = require("./attributes"); // javascript:* var jsInAttrRe = attributeHelpers.jsInAttrRe; // the list of all available event attributes var intrinsicEvents = attributeHelpers.intrinsicEvents; var privacyChecker = require("../../js_checker/privacy_checker").privacyCheck; const types = require("../../js_checker/constant_types"); var checkTypes = types.checkTypes; // array reflex valid types as listed in // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsScriptLoader.cpp#437 // anything appended to end of strings is considered valid: var jsValidTypes = [ /^text\/javascript/i, /^text\/ecmascript/i, /^application\/javascript/i, /^application\/ecmascript/i, /^application\/x-javascript/i ]; var stripCDATAOpen = /<\!\[CDATA\[/gi; var stripCDATAClose = /]]>/g; var stripHtmlCommentsInScript = function (s) { s = s.replace(stripCDATAOpen, ''); s = s.replace(stripCDATAClose, ''); return s; }; // gather scripts and javascript in attributes across a dom object. var DomGatherer = function() { // domHandler object. this.d = null; }; /** * init * * assign a reference domHandler object * to access/updates its properties. * */ DomGatherer.prototype.init = function (domHandler) { this.d = domHandler; }; /** * scriptHasInvalidType * * Checks that a script does not have a js "template" type. * Normally any script that has a type attribute other than the * few allowed ones is not interpreted. But by security, we only * discard a few of them. * * @param script obj The script element. * @return returns true if it matches a template type. * */ DomGatherer.prototype.scriptHasInvalidType = function (script) { var i = 0, le = jsValidTypes.length; var type = script.getAttribute('type'); if (type === 'librejs/blocked') { // js has already been blocked. return true; } if (!type) { // type isn't set, don't look further. return false; } for (; i < le; i++) { if (jsValidTypes[i].test(type)) { return false; } } // type is invalid and // hence cannot be executed. return true; }; /** * findScripts * * Assigns the array of scripts in the dom to a property * as well as a number of scripts present for looping purposing. */ DomGatherer.prototype.findScripts = function() { this.d.domScripts = this.d.dom.getElementsByTagName('script'); this.d.numScripts = this.d.domScripts.length; }; /** * gatherIntrinsicEvents * * Fetches all the event attributes that might contain JavaScript * as well as all element attributes that start with * "javascript:". * */ DomGatherer.prototype.gatherIntrinsicEvents = function() { var i = 0, j, k, all = this.d.dom.getElementsByTagName('*'), max = all.length, that = this, attrLen, attrib, str, scriptEntry; for (; i < max; i++) { // look for attributes with value javascript:* attributeHelpers.findJSinAttribute( all[i], function (scriptEntry) { if (scriptEntry !== false) { that.d.inlineScripts.push(scriptEntry); that.d.scripts.push(scriptEntry); // add inline script in the count. that.d.numInlineScripts++; } }); // look for attributes of on* (onLoad, ...) attributeHelpers.findOnJSAttribute( all[i], function (scriptEntry) { if (scriptEntry !== false) { that.d.inlineScripts.push(scriptEntry); that.d.scripts.push(scriptEntry); // add inline script in the count. that.d.numInlineScripts++; } }); } }; /** * gatherScriptsContent * * Aggregate all content within on-page JavaScript code. * Keep a list of all absolute urls to external scripts. * */ DomGatherer.prototype.gatherScriptsContent = function() { var i = 0, currentScript = '', absolutePath, scriptEntry, that = this; try { for (; i < this.d.numScripts; i++) { if (this.d.checkScriptForJsWebLabels(this.d.domScripts[i])) { //break; absolutePath = urlHandler.resolve( this.d.pageURL, this.d.domScripts[i].src); scriptEntry = scriptObject.Script( {'type': scriptTypes.EXTERNAL, 'status': statusTypes.JSWEBLABEL, 'element': this.d.domScripts[i], 'url': absolutePath}); scriptEntry.tree = {}; this.d.externalScripts.push(scriptEntry); that.d.scripts.push(scriptEntry); this.d.loadsHtmlExternalScripts = true; // increment number of scripts found. this.d.numExternalScripts++; } // check that script has valid type else if (!this.scriptHasInvalidType(this.d.domScripts[i])) { if (this.d.hasSrc(this.d.domScripts[i]) && !this.d.scriptHasJsWebLabel(this.d.domScripts[i])) { console.debug('an external script', this.d.domScripts[i]); absolutePath = urlHandler.resolve( this.d.pageURL, this.d.domScripts[i].src); scriptEntry = scriptObject.Script( {'type': scriptTypes.EXTERNAL, 'status': statusTypes.UNCHECKED, 'element': this.d.domScripts[i], 'url': absolutePath}); this.d.externalScripts.push(scriptEntry); that.d.scripts.push(scriptEntry); this.d.loadsHtmlExternalScripts = true; // increment number of scripts found. this.d.numExternalScripts++; } else if (privacyChecker.checkScriptPrivacyThreat(this.d.domScripts[i].text)) { this.d.removeGivenJs(scriptObject.Script( {'type': scriptTypes.SINGLETON, 'status': statusTypes.UNCHECKED, 'element': this.d.domScripts[i], 'text': this.d.domScripts[i].text }), '', true); } else if (this.d.domScripts[i].text !== '') { // using else if since script text is // ignored if src attribute is set. // adding this.narcissusBugFixLibreJS to fix comment bug. var bugfix = require('../../html_script_finder/bug_fix') .narcissusBugFixLibreJS; currentScript = stripHtmlCommentsInScript( this.d.domScripts[i].text + bugfix); scriptEntry = scriptObject.Script( {'type': scriptTypes.INLINE, 'status': statusTypes.UNCHECKED, 'element': this.d.domScripts[i], 'text': currentScript}); this.d.inlineScripts.push(scriptEntry); this.d.scripts.push(scriptEntry); // add inline script in the count. this.d.numInlineScripts++; } } } } catch (e) { // Any problem arising, we remove the script. console.debug('problem gathering scripts', e, e.lineNumber); this.d.removeAllJs(); } }; /* * exports.domGatherer * Instantiate a brand new clone of the domGatherer. * @param dom obj The given dom for analysis. * @param pageURL string the URL for the page. * @param callback function callback when all the work has been performed. */ exports.domGatherer = function (domHandler) { var dg = new DomGatherer(); dg.init(domHandler); return dg; };