From a49963548f9c5e3722d1d341129a783a4ec08258 Mon Sep 17 00:00:00 2001 From: IanNov <38321256+IanNov@users.noreply.github.com> Date: May 28 2020 11:13:56 +0000 Subject: Added base HTTP request protection functionality http_shield_firefox.js - contains Firefox specific functionality http_shield_chrome.js - contains Chrome specific functionality http_shield_common.js - contains common functions used by both versions Modified both manifests to fit the new files in it, extended permissions. Modified Makefile, so it check for .CSV files containing IPv4/v6 locally served zones at every make Updated update.js with added storage keys --- diff --git a/Makefile b/Makefile index c394a71..a9514e9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: firefox chrome +all: get_csv firefox chrome .PHONY: firefox chrome clean firefox: firefox_JSR.zip @@ -10,6 +10,11 @@ COMMON_FILES = $(shell find common/) \ $(shell find firefox/) \ $(shell find chrome/) +get_csv: + wget -q -N --directory-prefix=./common/ https://www.iana.org/assignments/locally-served-dns-zones/ipv4.csv + wget -q -N --directory-prefix=./common/ https://www.iana.org/assignments/locally-served-dns-zones/ipv6.csv + + %_JSR.zip: $(COMMON_FILES) @rm -rf $*_JSR/ $@ @cp -r common/ $*_JSR/ diff --git a/chrome/http_shield_chrome.js b/chrome/http_shield_chrome.js new file mode 100644 index 0000000..c14ca8f --- /dev/null +++ b/chrome/http_shield_chrome.js @@ -0,0 +1,494 @@ +// +// JavaScript Restrictor is a browser extension which increases level +// of security, anonymity and privacy of the user while browsing the +// internet. +// +// Copyright (C) 2020 Pavel Pohner +// +// 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 . +// + +// Implementation of HTTP webRequest shield, file: http_shield_chrome.js +// Contains Chrome specific functions +// Event handlers for webRequest API, notifications and messaging + +/// Associative array of hosts, that are currently blocked based on their previous actions +var blockedHosts = new Object(); +/// Information about hosts, for which cant be used DNS query +var hostStatistics = new Object(); + +/// Percentage of hosts, that can register an HTTP error response +var uniqueErrorHostsRatio = 10.0; +/// If there are more hosts than uniqueErrorHostsLimit which are targeted from the same origin, the origin host becomes blocked +var uniqueErrorHostsLimit = 20; +/// Number of Request Timed Out errors allowed for one origin +var errorsAllowed = 10; +/// Number of HTTP client errors (eg. 404 not found, 403 forbidden etc.) per requestTimeInterval +var httpClientErrorsAllowed = 5; + +/// Errors that are considered as possible attacker threat +var httpErrorList = { + 400:true, + 404:true, + 405:true, + 406:true, + 408:true, + 410:true, + 413:true, + 414:true, + 415:true, + 501:true, + 503:true, + 505:true +}; + +/// String that defines Request Timed Out error in Chrome +/// according to: https://developer.chrome.com/extensions/webRequest#event-onErrorOccurred +/// It's not backwards compatible, but it's the best we have +var chromeErrorString = "net::ERR_CONNECTION_TIMED_OUT"; + +/// If the browser regained connectivity - came online +window.addEventListener("online", function() +{ + //Hook up the listener to the onErrorOccured webRequest event + browser.webRequest.onErrorOccurred.addListener( + onErrorOccuredListener, + {urls: [""]} + ); +}); + +/// If the browser lost connectivity - gone offline +window.addEventListener("offline", function() +{ + //Disconnect the listener from the onErrorOccured webRequest event + browser.webRequest.onErrorOccurred.removeListener(onErrorOccuredListener); +}); + +/// Check the storage for requestShieldOn boolean +browser.storage.sync.get(["requestShieldOn"], function(result){ + //If not found (initialization) or true + if (result.requestShieldOn == undefined || result.requestShieldOn) + { + //Hook up the listeners + browser.webRequest.onBeforeSendHeaders.addListener( + beforeSendHeadersListener, + {urls: [""]}, + ["blocking", "requestHeaders"] + ); + + browser.webRequest.onHeadersReceived.addListener( + onHeadersReceivedRequestListener, + {urls: [""]}, + ["blocking"] + ); + + browser.webRequest.onErrorOccurred.addListener( + onErrorOccuredListener, + {urls: [""]} + ); + } +}); + +/// Hook up the listener for receiving messages +browser.runtime.onMessage.addListener(messageListener); + +/// webRequest event listener, hooked to onErrorOccured event +/// Catches all errors, checks them for Request Timed out errors +/// Iterates error counter, blocks the host if limit was exceeded +/// Takes object representing error in responseDetails variable +function onErrorOccuredListener(responseDetails) { + + //It's neccessary to have both of these defined, otherwise the error can't be analyzed + if (responseDetails.initiator === undefined || responseDetails.url === undefined) + { + return {cancel:false}; + } + var sourceUrl = new URL(responseDetails.initiator); + //Removing www. from hostname, so the hostnames are uniform + sourceUrl.hostname = sourceUrl.hostname.replace(/^www\./,''); + var targetUrl = new URL(responseDetails.url); + targetUrl.hostname = targetUrl.hostname.replace(/^www\./,''); + + //Host found among user's trusted hosts, allow it right away + if (checkWhitelist(sourceUrl.hostname)) + { + return {cancel:false}; + } + //Host found among user's untrusted, thus blocked, hosts, blocking it without further actions + if (blockedHosts[sourceUrl.hostname] != undefined) + { + return {cancel:true}; + } + + //If the error is TIMED_OUT -> access to non-existing IP + if (responseDetails.error == chromeErrorString) + { + //Count erros for given host + if (hostStatistics[sourceUrl.hostname] != undefined) + { + hostStatistics[sourceUrl.hostname]["errors"] += 1; + } + else + { + hostStatistics[sourceUrl.hostname] = insertHostInStats(targetUrl.hostname); + hostStatistics[sourceUrl.hostname]["errors"] = 1; + } + //Block the host if the error limit was exceeded + if(hostStatistics[sourceUrl.hostname]["errors"] > errorsAllowed) + { + notifyBlockedHost(sourceUrl.hostname); + blockedHosts[sourceUrl.hostname] = true; + } + + } + return {cancel:false}; +} + +/// webRequest event listener, hooked to onErrorOccured event +/// Catches all responses, analyzes those with record in hostStatistics +/// Modifies counters, blocks if one of the limits was exceeded +function onHeadersReceivedRequestListener(headers) +{ + //It's neccessary to have both of these defined, otherwise the response can't be analyzed + if (headers.initiator === undefined || headers.url === undefined) + { + return {cancel:false}; + } + + var sourceUrl = new URL(headers.initiator); + //Removing www. from hostname, so the hostnames are uniform + sourceUrl.hostname = sourceUrl.hostname.replace(/^www\./,''); + var targetUrl = new URL(headers.url); + targetUrl.hostname = targetUrl.hostname.replace(/^www\./,''); + + //Host found among user's trusted hosts, allow it right away + if (checkWhitelist(sourceUrl.hostname)) + { + return {cancel:false}; + } + + //Host found among user's untrusted, thus blocked, hosts, blocking it without further actions + if (blockedHosts[sourceUrl.hostname] != undefined) + { + return {cancel:true}; + } + + //If it's the error code that exists in httpErrorList + if (httpErrorList[headers.statusCode] != undefined) + { + //Obtain record for given origin from statistics array + //Record has to be there already, because it was inserted there while + //encountering the request from this origin + var currentHost = hostStatistics[sourceUrl.hostname]; + + //Check if the target domain was already encountered for this source origin + if (currentHost[targetUrl.hostname] != undefined) + { + //If so, iterate http errors variable for this origin and target domain + currentHost[targetUrl.hostname]["httpErrors"] += 1; + //If it's firt error from this target + if(!currentHost[targetUrl.hostname]["hadError"]) + { + //Iterate global counter for this source origin + currentHost["httpErrors"] += 1; + //Set that we've seen the error from this target already + currentHost[targetUrl.hostname]["hadError"] = true; + + //Allow atleast one error hosts, if 10% ratio is less than one error host + //Set hosts to 10, if there are less than 10 hosts + var hosts = currentHost["hosts"] < uniqueErrorHostsRatio ? uniqueErrorHostsRatio : currentHost["hosts"]; + var errors = currentHost["httpErrors"]; + var errorRatio = errors*1.0 / hosts * 100; + //If the ratio, or the fixed limit for source origin was exceeded + if (errorRatio > uniqueErrorHostsRatio || errors > uniqueErrorHostsLimit) + { + //Block the origin + notifyBlockedHost(sourceUrl.hostname); + blockedHosts[sourceUrl.hostname] = true; + return {cancel:true}; + } + } + //If the limit for http error response from target host was exceeded + if(currentHost[targetUrl.hostname]["httpErrors"] > httpClientErrorsAllowed) + { + //Block the origin + notifyBlockedHost(sourceUrl.hostname); + blockedHosts[sourceUrl.hostname] = true; + return {cancel:true}; + } + } + } + //Successful response + else if ((headers.statusCode >= 100) && (headers.statusCode < 400)) + { + //Obtain record for given origin from statistics array + var currentHost = hostStatistics[sourceUrl.hostname]; + //Check if we've seen this target for given source origin + if (currentHost[targetUrl.hostname] != undefined) + { + //if so, check if it's the first successful response from this target URL + if (currentHost[targetUrl.hostname]["successfulResponses"][targetUrl] === undefined) + { + //If so, note that we've seen this URL already + currentHost[targetUrl.hostname]["successfulResponses"][targetUrl] = 1; + //Decrement the counter + currentHost[targetUrl.hostname]["httpErrors"] -= 0.5; + } + else + { + currentHost[targetUrl.hostname]["successfulResponses"][targetUrl] += 1; + } + + //Normalize the number, if it's less than zero + if (currentHost[targetUrl.hostname]["httpErrors"] < 0) + currentHost[targetUrl.hostname]["httpErrors"] = 0; + } + } + return {cancel:false}; +} + +/// Function that creates object representing source host +/// Recieves target hostname in targetDomain argument +function insertHostInStats(targetDomain) +{ + var currentHost = new Object(); + currentHost[targetDomain] = new Object(); + currentHost[targetDomain]["requests"] = 1; + currentHost[targetDomain]["httpErrors"] = 0; + currentHost[targetDomain]["hadError"] = false; + currentHost[targetDomain]["successfulResponses"] = new Object(); + currentHost["hosts"] = 1; + currentHost["requests"] = 1; + currentHost["httpErrors"] = 0; + currentHost["errors"] = 0; + + return currentHost; +} + +/// webRequest event listener, hooked to onBeforeSendHeaders event +/// Catches all requests, analyzes them, does blocking, +/// modifies counters, blocks if one of the limits was exceeded +function beforeSendHeadersListener(requestDetail) { + + //It's neccessary to have both of these defined, otherwise the response can't be analyzed + if (requestDetail.initiator === undefined || requestDetail.url === undefined) + { + return {cancel:false}; + } + + var sourceUrl = new URL(requestDetail.initiator); + //Removing www. from hostname, so the hostnames are uniform + sourceUrl.hostname = sourceUrl.hostname.replace(/^www\./,''); + var targetUrl = new URL(requestDetail.url); + targetUrl.hostname = targetUrl.hostname.replace(/^www\./,''); + + var isSourcePrivate = false; + var isDestinationPrivate = false; + + //Host found among user's trusted hosts, allow it right away + if (checkWhitelist(sourceUrl.hostname) != undefined) + { + return {cancel:false}; + } + + //Host found among user's untrusted, thus blocked, hosts, blocking it without further actions + if (blockedHosts[sourceUrl.hostname] != undefined) + { + return {cancel:true}; + } + + //Checking type of SOURCE URL + if (isIPV4(sourceUrl.hostname)) //SOURCE is IPV4 adddr + { + //Checking privacy of IPv4 + if (isIPV4Private(sourceUrl.hostname)) + { + //Source is IPv4 private + isSourcePrivate = true; + } + } + else if(isIPV6(sourceUrl.hostname)) //SOURCE is IPV6 + { + //Checking privacy of IPv6 + if (isIPV6Private(sourceUrl.hostname)) + { + //Source is IPv4 private + isSourcePrivate = true; + } + } + else //SOURCE is hostname + { + //Checking if target URL is IPv4 private or IPv6 private + if ((isIPV4(targetUrl.hostname) && isIPV4Private(targetUrl.hostname)) || + (isIPV6(targetUrl.hostname) && isIPV6Private(targetUrl.hostname))) + { + //If so, block the request - strict + notifyBlockedRequest(sourceUrl.hostname, targetUrl.hostname, requestDetail.type); + return {cancel:true}; + } + //Target is either host name or public IP + var currentHost = hostStatistics[sourceUrl.hostname]; + + //If its the first time we're seeing this source host + if (currentHost == undefined) + { + currentHost = insertHostInStats(targetUrl.hostname); + hostStatistics[sourceUrl.hostname] = currentHost; + return {cancel:false}; + } + //Check if we've seen this target for this source host + if (currentHost[targetUrl.hostname] != undefined) + { + currentHost[targetUrl.hostname].requests += 1; + } + else //If not, just insert the stats + { + currentHost[targetUrl.hostname] = new Object(); + currentHost[targetUrl.hostname]["requests"] = 1; + currentHost[targetUrl.hostname]["httpErrors"] = 0; + currentHost[targetUrl.hostname]["hadError"] = false; + currentHost[targetUrl.hostname]["successfulResponses"] = new Object(); + currentHost["hosts"] += 1; + } + return {cancel:false}; + } + //Check the target domain + if (isIPV4(targetUrl.hostname)) + { + if (isIPV4Private(targetUrl.hostname)) + { + //Its private IPv4 + isDestinationPrivate = true; + } + } + else if(isIPV6(targetUrl.hostname)) + { + if (isIPV6Private(targetUrl.hostname)) + { + //Its private IPv6 + isDestinationPrivate = true; + } + } + //Blocking direction Public -> Private + if (!isSourcePrivate && isDestinationPrivate) + { + notifyBlockedRequest(sourceUrl.hostname, targetUrl.hostname, requestDetail.type); + return {cancel:true} + } + else //Permitting others + { + return {cancel: false}; + } +} + +/// Creates and presents notification to the user +/// works with webExtensions notification API +function notifyBlockedRequest(origin, target, resource) { + browser.notifications.create({ + "type": "basic", + "iconUrl": browser.extension.getURL("img/icon-48.png"), + "title": "Blocked suspicious request!", + "message": "Request from originUrl " + origin + " to targetUrl " + target + " was blocked. If it's unwanted behaviour, please go to options and add an exception." + }); +} + +/// Creates and presents notification to the user +/// works with webExtensions notification API +function notifyBlockedHost(host) { + browser.notifications.create({ + "type": "basic", + "iconUrl": browser.extension.getURL("img/icon-48.png"), + "title": "Host was blocked!", + "message": "Host: " + host + " was blocked based on suspicious actions. If it's unwanted behaviour, please add an exception in options or popup." + }); +} + +/// webRequest event listener, hooked to onMessage event +/// obtains message string in message, message sender in sender +/// and function for sending response in sendResponse +/// Does approriate action based on message text +function messageListener(message, sender, sendResponse) +{ + //Message sent from options.js, whitelist was updated + if (message.message === "whitelist updated") + { + //Updating whitelist from storage + browser.storage.sync.get(["whitelistedHosts"], function(result){ + doNotBlockHosts = result.whitelistedHosts; + }); + } + //HTTP request shield was turned on + else if (message.message === "turn request shield on") + { + //Hook up the listeners + browser.webRequest.onBeforeSendHeaders.addListener( + beforeSendHeadersListener, + {urls: [""]}, + ["blocking", "requestHeaders"] + ); + + browser.webRequest.onHeadersReceived.addListener( + onHeadersReceivedRequestListener, + {urls: [""]}, + ["blocking"] + ); + + browser.webRequest.onErrorOccurred.addListener( + onErrorOccuredListener, + {urls: [""]} + ); + } + //HTTP request shield was turned off + else if (message.message === "turn request shield off") + { + //Disconnect the listeners + browser.webRequest.onBeforeSendHeaders.removeListener(beforeSendHeadersListener); + browser.webRequest.onHeadersReceived.removeListener(onHeadersReceivedRequestListener); + browser.webRequest.onErrorOccurred.removeListener(onErrorOccuredListener); + } + //Mesage came from popup.js, whitelist this site + else if (message.message === "add site to whitelist") + { + //Obtain current hostname and whitelist it + var currentHost = message.site; + doNotBlockHosts[currentHost] = true; + browser.storage.sync.set({"whitelistedHosts":doNotBlockHosts}); + } + //Message came from popup.js, remove whitelisted site + else if (message.message === "remove site from whitelist") + { + //Obtain current hostname and remove it + currentHost = message.site; + delete doNotBlockHosts[currentHost]; + browser.storage.sync.set({"whitelistedHosts":doNotBlockHosts}); + } + //Message came from popup,js, asking whether is this site whitelisted + else if (message.message === "is current site whitelisted?") + { + //Read the current hostname + var currentHost = message.site; + //Response with appropriate message + if (checkWhitelist(currentHost)) + { + sendResponse("current site is whitelisted"); + return true; + } + else + { + sendResponse("current site is not whitelisted"); + return true; + } + } +} + diff --git a/chrome/manifest.json b/chrome/manifest.json index 47371af..2583c02 100644 --- a/chrome/manifest.json +++ b/chrome/manifest.json @@ -1,7 +1,7 @@ { - "author": "Libor Polčák, Martin Timko", + "author": "Libor Polčák, Martin Timko, Pavel Pohner", "background": { - "scripts": ["update.js", "url.js", "levels.js", "background.js"], + "scripts": ["update.js", "url.js", "levels.js", "background.js", "http_shield_common.js", "http_shield_chrome.js"], "persistent": true }, "browser_action": { @@ -40,7 +40,7 @@ "manifest_version": 2, "name": "JavaScript Restrictor", "options_page": "options.html", - "permissions": ["storage", "tabs"], + "permissions": ["storage", "tabs", "webRequest", "webRequestBlocking", "proxy", "", "notifications"], "short_name": "JSR", "version": "0.3" } diff --git a/common/http_shield_common.js b/common/http_shield_common.js new file mode 100644 index 0000000..6aa369a --- /dev/null +++ b/common/http_shield_common.js @@ -0,0 +1,339 @@ +// +// JavaScript Restrictor is a browser extension which increases level +// of security, anonymity and privacy of the user while browsing the +// internet. +// +// Copyright (C) 2020 Pavel Pohner +// +// 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 . +// + +// Implementation of HTTP webRequest shield, file: http_shield_common.js +// Contains common functions for both versions - Chrome and Firefox. +// Mainly for reading CSV files, and checking IP ranges. + +//Chrome compatibility +if ((typeof browser) === "undefined") { + var browser = chrome; +} + +/// Locally served IPV4 DNS zones loaded from IANA +var localIPV4DNSZones; +/// Locally served IPV6 DNS zones loaded from IANA +var localIPV6DNSZones; + +/// Associtive array of hosts, that are currently among trusted "do not blocked" hosts +var doNotBlockHosts = new Object(); +browser.storage.sync.get(["whitelistedHosts"], function(result){ + if (result.whitelistedHosts != undefined) + doNotBlockHosts = result.whitelistedHosts; + }); + +/// Function for reading locally stored csv file +let readFile = (_path) => { + return new Promise((resolve, reject) => { + //Fetching locally stored CSV file in same-origin mode + fetch(_path, {mode:'same-origin'}) + .then(function(_res) { + //Return data as a blob + return _res.blob(); + }) + .then(function(_blob) { + var reader = new FileReader(); + //Wait until the whole file is read + reader.addEventListener("loadend", function() { + resolve(this.result); + }); + //Read blob data as text + reader.readAsText(_blob); + }) + .catch(error => { + reject(error); + }); + }); +}; + +/// Obtain file path in user's file system and read CSV file with IPv4 local zones +readFile(browser.runtime.getURL("ipv4.csv")) + .then(_res => { + //Parse loaded CSV and store it in prepared variable + localIPV4DNSZones = parseCSV(_res, true); + }) + .catch(_error => { + console.log(_error ); + }); + +/// Obtain file path in user's file system and read CSV file with IPv6 local zones +readFile(browser.runtime.getURL("ipv6.csv")) + .then(_res => { + //Parse loaded CSV and store it in prepared variable + localIPV6DNSZones = parseCSV(_res, false); + }) + .catch(_error => { + console.log(_error ); + }); + +/// Checks validity of IPv4 addresses, +/// returns TRUE if the url matches IPv4 regex +/// FALSE otherwise +function isIPV4(url) +{ + var reg = new RegExp("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); + return reg.test(url); +} + +/// Checks validity IPV6 address +/// Returns TRUE, if URL is valid IPV6 address +/// FALSE otherwise +function isIPV6(url) +{ + url = url.substring(1, url.length - 1); + var reg = new RegExp("^(?:(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-fA-F]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,1}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,2}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,3}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:[0-9a-fA-F]{1,4})):)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,4}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,5}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,6}(?:(?:[0-9a-fA-F]{1,4})))?::))))$", 'm'); + return reg.test(url); +} + +/// Checks whether the ipAddr is found in IPv4 localZones +/// Returns TRUE if ipAddr exists in localZones fetched from IANA +/// FALSE otherwise +/// This function should only be called on valid IPv4 address +function isIPV4Private(ipAddr) +{ + //Split IP address on dots, obtain 4 numbers + var substrIP = ipAddr.split('.'); + //Convert IP address into array of 4 integers + var ipArray = substrIP.map(function(val){ + return parseInt(val, 10); + }); + //For each IPv4 locally served zone + for (var i = 0; i < localIPV4DNSZones.length; i++) + { + //Split the zone into array of J numbers + var zone = localIPV4DNSZones[i].split('.'); + var k = 0; + //For each number of local zone IP + //(Decrementing, because local zones IPs are reverted + for (var j = zone.length - 1; j >= 0; j--) + { + //Check if the corresponding numbers match + //If not, then break and move onto next local zone + if (ipArray[k] != zone[j]) + { + break; + } + else if(j == 0) //Checked all numbers of local zone + { + return true; + } + k++; + } + } + return false; +} + +/// Checks whether the ipAddr is found in IPv6 localZones +/// Returns TRUE if ipAddr exists in localZones fetched from IANA +/// FALSE otherwise +/// This function should only be called on valid IPv6 address +function isIPV6Private(ipAddr) +{ + //Expand shorten IPv6 addresses to full length + ipAddr = expandIPV6(ipAddr); + //Split into array of fields + var substrIP = ipAddr.split(":"); + //Join the fields into one string + ipAddr = substrIP.join("").toUpperCase(); + //For each IPv6 locally served zone + for (var i = 0; i < localIPV6DNSZones.length; i++) + { + var zone = localIPV6DNSZones[i]; + //For each char of zone + for (var j = 0; j < zone.length; j++) + { + //Compare the chars, if they do not match, break and move onto next zone + if (ipAddr.charAt(j) != zone.charAt(j)) + { + break; + } + //Checked all chars of current zone -> private IP range + else if(j == zone.length - 1) + { + return true; + } + } + } + return false; +} + +/// Function for parsing CSV files obtained from IANA +/// Strips .IN-ADDR and .IP6 from zones and comma delimiter, +/// merges them into array by CSV rows +/// Arguments: csv - CSV obtained from IANA +/// ipv4 - bool, saying whether the csv is IPv4 CSV or IPv6 +/// Returns: Array of parsed CSV values +function parseCSV(csv, ipv4) +{ + //converting into array + var csvArray = CSVToArray(csv); + var DNSzones = []; + + if (ipv4) //ipv4.csv + { + //cycle through first column of the CSV -> obtaining IP zones + //Starting with i = 1, skipping the CSV header + for (var i = 1; i < csvArray.length; i++) + { + //i-1, means start from 0 + //Obtains IP zone, strips .IN-ADDR from the end of it, stroes into array + DNSzones[i-1] = csvArray[i][0].substring(0, csvArray[i][0].indexOf(".IN-ADDR")); + } + return DNSzones; + } + else //ipv6.csv + { + //Same as ipv4 + for (var i = 1; i < csvArray.length-1; i++) + { + DNSzones[i-1] = csvArray[i][0].substring(0, csvArray[i][0].indexOf(".IP6")); + } + + for (var i = 0; i < DNSzones.length; i++) + { + //Additionally splits the IP zone on dots + var splitted = DNSzones[i].split("."); + DNSzones[i] = ""; + //Joins splitted IP zone into one string + for (var j = splitted.length - 1; j >= 0 ; j--) + { + DNSzones[i] += splitted[j]; + + } + } + return DNSzones; + } +} + +/// Auxillary function for parsing CSV files +/// Converts CSV to array +/// strData - loaded CSV file +/// Returns array containing CSV rows +function CSVToArray(strData){ + // Create a regular expression to parse the CSV values. + var objPattern = new RegExp( + ( + // Delimiters. + "(\\,|\\r?\\n|\\r|^)" + + // Quoted fields. + "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + + // Standard fields. + "([^\"\\,\\r\\n]*))" + ), + "gi" + ); + //Array to hold data + var csvData = [[]]; + //Array to hold regex matches + var regexMatches = null; + //While not match + while (regexMatches = objPattern.exec(strData)){ + // Get the delimiter that was found + var strMatchedDelimiter = regexMatches[1]; + if (strMatchedDelimiter.length && (strMatchedDelimiter != ",")){ + //New row + csvData.push([]); + } + // captured data (quoted or unquoted) + if (regexMatches[2]){ + //quoted + var strMatchedValue = regexMatches[2].replace( + new RegExp( "\"\"", "g" ), + "\"" + ); + } else { + //non-quoted value. + var strMatchedValue = regexMatches[3]; + + } + //Add to data array + csvData[csvData.length - 1].push( strMatchedValue ); + } + // Return the parsed data + return( csvData ); +} + +/// Function for expanding shorten ipv6 addresses +/// Takes valid ipv6 address in ip6addr argument +/// Returns expanded ipv6 address in string +/// This function should be only called on valid IPv6 address +function expandIPV6(ip6addr) +{ + ip6addr = ip6addr.substring(1, ip6addr.length - 1); + var expandedIP6 = ""; + //Check for omitted groups of zeros (::) + if (ip6addr.indexOf("::") == -1) + { + //There are none omitted groups of zeros + expandedIP6 = ip6addr; + } + else + { + //Split IP on one compressed group + var splittedIP = ip6addr.split("::"); + var amountOfGroups = 0; + //For each group + for (var i = 0; i < splittedIP.length; ++i) + { + //Split on : + amountOfGroups += splittedIP[i].split(":").length; + } + expandedIP6 += splittedIP[0] + ":"; + //For each splitted group + for (var i = 0; i < 8 - amountOfGroups; ++i) + { + //insert zeroes + expandedIP6 += "0000:"; + } + //Insert the rest of the splitted IP + expandedIP6 += splittedIP[1]; + } + //Split expanded IPv6 into parts + var addrParts = expandedIP6.split(":"); + var addrToReturn = ""; + //For each part + for (var i = 0; i < 8; ++i) + { + //check the length of the part + while(addrParts[i].length < 4) + { + //if it's less than 4, insert zero + addrParts[i] = "0" + addrParts[i]; + } + addrToReturn += i != 7 ? addrParts[i] + ":" : addrParts[i]; + } + return addrToReturn; +} + +//Check if the hostname or any of it's domains is whitelisted +function checkWhitelist(hostname) +{ + //Calling a function from url.js + var domains = extractSubDomains(hostname); + for (var domain of domains) + { + if (doNotBlockHosts[domain] != undefined) + { + return true; + } + } + return false; +} diff --git a/common/update.js b/common/update.js index 976da31..46b5f1a 100644 --- a/common/update.js +++ b/common/update.js @@ -40,6 +40,8 @@ function installUpdate() { * domains: {}, // associative array of levels associated with specific domains (key, the domain => object) * {level_id: short string of the level in use * } + * whitelistedHosts: {} // associative array of hosts that are removed from http protection control (hostname => boolean) + * requestShieldOn: {} // Boolean, if it's TRUE or undefined, the http request protection is turned on, if it's FALSE, the protection si turned off */ browser.storage.sync.get(null, function (item) { if (!item.hasOwnProperty("version")) { diff --git a/firefox/http_shield_firefox.js b/firefox/http_shield_firefox.js new file mode 100644 index 0000000..d093ffe --- /dev/null +++ b/firefox/http_shield_firefox.js @@ -0,0 +1,264 @@ +// +// JavaScript Restrictor is a browser extension which increases level +// of security, anonymity and privacy of the user while browsing the +// internet. +// +// Copyright (C) 2020 Pavel Pohner +// +// 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 . +// + +/// Implementation of HTTP webRequest shield, file: http_shield_firefox.js +/// Contains Firefox specific functions +/// Event handlers for webRequest API, notifications and messaging + +/// Hooking up the messageListener to message event +browser.runtime.onMessage.addListener(messageListener); + +/// Check the storage for requestShieldOn object +browser.storage.sync.get(["requestShieldOn"], function(result){ + //If found object is true or undefined, turn the requestShieldOn + if (result.requestShieldOn == undefined || result.requestShieldOn) + { + //Hookup the event handler for event onBeforeSendHeaders from webRequest API + browser.webRequest.onBeforeSendHeaders.addListener( + beforeSendHeadersListener, + {urls: [""]}, + ["blocking", "requestHeaders"] + ); + } +}); + + +/// webRequest event listener +/// Listens to onBeforeSendHeaders event, receives detail of HTTP request in requestDetail +/// Catches the request, analyzes it's origin and target URLs and blocks it/permits it based +/// on their IP adresses. Requests coming from public IP ranges targeting the private IPs are +/// blocked by default. Others are permitted by default. +async function beforeSendHeadersListener(requestDetail) { + + //If either of information is undefined, permit it + //originUrl happens to be undefined for the first request of the page loading the document itself + if (requestDetail.originUrl === undefined || requestDetail.url === undefined) + { + return {cancel:false}; + } + var sourceUrl = new URL(requestDetail.originUrl); + //Removing www. from hostname, so the hostnames are uniform + sourceUrl.hostname = sourceUrl.hostname.replace(/^www\./,''); + var targetUrl = new URL(requestDetail.url); + targetUrl.hostname = targetUrl.hostname.replace(/^www\./,''); + + var targetIP; + var sourceIP; + var isSourcePrivate = false; + var isDestinationPrivate = false; + var destinationResolution = ""; + var sourceResolution = ""; + + //Host found among user's trusted hosts, allow it right away + if (checkWhitelist(sourceUrl.hostname)) + { + return {cancel:false}; + } + + //Checking type of SOURCE URL + if (isIPV4(sourceUrl.hostname)) //SOURCE is IPV4 adddr + { + //Checking privacy of IPv4 + if (isIPV4Private(sourceUrl.hostname)) + { + //Source is IPv4 private + isSourcePrivate = true; + } + } + else if(isIPV6(sourceUrl.hostname)) //SOURCE is IPV6 + { + //Checking privacy of IPv6 + if (isIPV6Private(sourceUrl.hostname)) + { + //Source is IPv6 private + isSourcePrivate = true; + } + } + else //SOURCE is hostname + { + //Resoluting DNS query for source domain + sourceResolution = browser.dns.resolve(sourceUrl.hostname).then((val) => + { + //Assigning source IPs + sourceIP = val; + //More IPs could have been found, for each of them + for (let ip of sourceIP.addresses) + { + //Check whether it's IPv4 + if (isIPV4(ip)) + { + if (isIPV4Private(ip)) + { + //Source is IPv4 private + isSourcePrivate = true; + } + } + else if (isIPV6(ip)) + { + if (isIPV6Private(ip)) + { + //Source is IPv6 private + isSourcePrivate = true; + } + } + } + }); + } + + //Analyzing targetUrl + //Check IPv4/IPv6 and privacy + if (isIPV4(targetUrl.hostname)) + { + if (isIPV4Private(targetUrl.hostname)) + { + isDestinationPrivate = true; + + } + } + else if(isIPV6(targetUrl.hostname)) + { + if (isIPV6Private(targetUrl.hostname)) + { + isDestinationPrivate = true; + } + } + else //Target is hostname + { + //Resoluting DNS query for destination domain + destinationResolution = browser.dns.resolve(targetUrl.hostname).then((val) => + { + //Assigning source IPs + targetIP = val; + //More IPs could have been found, for each of them + for (let ip of targetIP.addresses) + { + //Checking type + if (isIPV4(ip)) + { + //Checking privacy + if (isIPV4Private(ip)) + { + isDestinationPrivate = true; + } + } + else if (isIPV6(ip)) + { + if (isIPV6Private(ip)) + { + isDestinationPrivate = true; + } + } + } + }); + } + //Wait till all DNS resolutions are done, because its neccessary for upcoming actions + await Promise.all([sourceResolution, destinationResolution]); + + //Blocking direction Public -> Private + if (!isSourcePrivate && isDestinationPrivate) + { + notify(sourceUrl.hostname, targetUrl.hostname, requestDetail.type); + return {cancel:true} + } + else //Permitting others + { + return {cancel: false}; + } +} + +/// Function that works with webExtensions notifications +/// Creates notification about blocked request +/// Arguments: +/// origin - origin of the request +/// target - target of the request +/// resource - type of the resource +function notify(origin, target, resource) { + browser.notifications.create({ + "type": "basic", + "iconUrl": browser.extension.getURL("img/icon-48.png"), + "title": "Blocked suspicious request!", + "message": "Request from originUrl " + origin + " to targetUrl " + target + " was blocked. If it's unwanted behaviour, please go to options and add an exception." + }); +} + +/// Event listener hooked up to webExtensions onMessage event +/// Receives full message in message, +/// sender of the message in sender, +/// function for sending response in sendResponse +/// Does appropriate action based on message +async function messageListener(message, sender, sendResponse) +{ + //Message came from options.js, updated whitelist + if (message.message === "whitelist updated") + { + //actualize current doNotBlockHosts from storage + browser.storage.sync.get(["whitelistedHosts"], function(result){ + doNotBlockHosts = result.whitelistedHosts; + }); + } + //Message came from option.js, request shield was turned on + else if (message.message === "turn request shield on") + { + //Hook up the event handler + browser.webRequest.onBeforeSendHeaders.addListener( + beforeSendHeadersListener, + {urls: [""]}, + ["blocking", "requestHeaders"] + ); + } + //Message came from option.js, request shield was turned off + else if (message.message === "turn request shield off") + { + //Remove event handler from onBeforeSendHeaders event + browser.webRequest.onBeforeSendHeaders.removeListener(beforeSendHeadersListener); + } + //Mesage came from popup.js, whitelist this site + else if (message.message === "add site to whitelist") + { + //Obtain current hostname and whitelist it + var currentHost = message.site; + doNotBlockHosts[currentHost] = true; + browser.storage.sync.set({"whitelistedHosts":doNotBlockHosts}); + } + //Message came from popup.js, remove whitelisted site + else if (message.message === "remove site from whitelist") + { + //Obtain current hostname and remove it + currentHost = message.site; + delete doNotBlockHosts[currentHost]; + browser.storage.sync.set({"whitelistedHosts":doNotBlockHosts}); + } + //Message came from popup,js, asking whether is this site whitelisted + else if (message.message === "is current site whitelisted?") + { + //Read the current hostname + var currentHost = message.site; + //Response with appropriate message + if (checkWhitelist(currentHost)) + { + return Promise.resolve("current site is whitelisted"); + } + else + { + return Promise.resolve("current site is not whitelisted"); + } + } +} diff --git a/firefox/manifest.json b/firefox/manifest.json index ae564dd..513160d 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -1,7 +1,7 @@ { - "author": "Libor Polčák, Martin Timko", + "author": "Libor Polčák, Martin Timko, Pavel Pohner", "background": { - "scripts": ["update.js", "url.js", "levels.js", "background.js"] + "scripts": ["update.js", "url.js", "levels.js", "background.js", "http_shield_common.js", "http_shield_firefox.js"] }, "browser_action": { "browser_style": true, @@ -43,7 +43,7 @@ "page": "options.html", "open_in_tab": true }, - "permissions": ["storage", "tabs"], + "permissions": ["storage", "tabs", "webRequest", "webRequestBlocking", "proxy", "dns", "", "notifications"], "short_name": "JSR", "version": "0.3", "browser_specific_settings": {