Skip to content
Snippets Groups Projects
Commit b936d16a authored by Anton Bershanskiy's avatar Anton Bershanskiy
Browse files

Added domain name status icons; started HSTS and thirdparty analysis

Added Font Awesome domain name status icons "secure", "enabled", "disabled", "insecure",
started implementing HSTS analysis (dynamic only, no preload lists).
Created definition of thirdparty sources and primitive implementation.
parent fd29229d
No related branches found
No related tags found
No related merge requests found
...@@ -13,9 +13,10 @@ ...@@ -13,9 +13,10 @@
}, },
"scripts": { "scripts": {
"test": "test", "test": "test",
"build": "webpack", "build": "webpack --mode=production",
"dev": "webpack --mode=development",
"clean": "rm -rf build;", "clean": "rm -rf build;",
"icons": "curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/question-circle-o.svg -o source/common/includes/fontawesome/question-circle-o.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/caret-down.svg -o source/common/includes/fontawesome/caret-down.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/caret-right.svg -o source/common/includes/fontawesome/caret-right.svg" "icons": "curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/question-circle-o.svg -o source/common/includes/fontawesome/question-circle-o.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/caret-down.svg -o source/common/includes/fontawesome/caret-down.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/caret-right.svg -o source/common/includes/fontawesome/caret-right.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/check.svg -o source/common/includes/fontawesome/check.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/lock.svg -o source/common/includes/fontawesome/lock.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/unlock.svg -o source/common/includes/fontawesome/unlock.svg && curl https://raw.githubusercontent.com/encharm/Font-Awesome-SVG-PNG/master/black/svg/exclamation-triangle.svg -o source/common/includes/fontawesome/exclamation-triangle.svg"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
......
...@@ -6,6 +6,8 @@ const cookie = require("cookie") ...@@ -6,6 +6,8 @@ const cookie = require("cookie")
const parseResponseHeaderStrictTransportSecurity = require("../libraries/hsts/hsts").parseResponseHeaderStrictTransportSecurity const parseResponseHeaderStrictTransportSecurity = require("../libraries/hsts/hsts").parseResponseHeaderStrictTransportSecurity
const thirdparty = require("../libraries/thirdparty/thirdparty").thirdparty
// Initialize the cookie database upon extension installation // Initialize the cookie database upon extension installation
const urlPopup = window.location.origin + "/pages/popup/popup.html" const urlPopup = window.location.origin + "/pages/popup/popup.html"
...@@ -13,14 +15,30 @@ const urlPopup = window.location.origin + "/pages/popup/popup.html" ...@@ -13,14 +15,30 @@ const urlPopup = window.location.origin + "/pages/popup/popup.html"
// TODO: make an actual datasructure. // TODO: make an actual datasructure.
var cookieDatabase = {"example":"example"} var cookieDatabase = {"example":"example"}
/* Structure of tabObservations is as follows: /* Structure of tabObservations is as follows:
tabObservations is a dictionaty with keys tabId and values tabObservations is a dictionaty with keys tabId and values objects
objects: originUrl (string) originUrl (string) - sanity check that page is correct
observations - array of arrays of observations domains - a dictionary with keys domains and values objects
thirdparty (boolean) - if this should be displayed on "Primary resources" or "Third-party resources"
this is assigned when record is created
status (string)
this is assigned/updated when record is requested by UI
status has one of the following values:
"secure" -- domain is already protected, e.g. by HSTS or CSP
"enabled" -- we protect vulnerable domain
"disabled" -- we let vulnerable domain be vulnerable
"insecure" -- domain does not support HTTPS (hopeless case)
// reason - string explanation, to be put on tooltip CSP HSTS
cookies - dictionary of cookies with key cookie name and value object
(it is summary to be displayed in the popup)
events - all things that ever happened
*/ */
var tabObservations = {} var tabObservations = {}
// Disctionary with keys domains and values the current HSTS record
// TODO: remember time of record creation
var hstsObservations = {}
/* /*
* Prepare datastructures of the newly installed extension * Prepare datastructures of the newly installed extension
*/ */
...@@ -52,9 +70,59 @@ platform.runtime.onInstalled.addListener (function() { ...@@ -52,9 +70,59 @@ platform.runtime.onInstalled.addListener (function() {
case "general": case "general":
console.log("Popup open: General") console.log("Popup open: General")
break break
// Prepare security report
case "security": case "security":
console.log("Popup open: Security", tabObservations) console.log("Popup open: Security, preparing report")
sendResponse(tabObservations[message.tabId]) let report = tabObservations[message.tabId]
console.log("REPORT BEFORE", report)
console.log(hstsObservations)
for (const domain in report.domains){
// Update domain status
switch(report.domains[domain].status){
case "secure":
case "disabled":
case "insecure":
case null:
// Need to update security assessment
// TODO: which check CSP and differentiate between "disabled" and "insecure"
// REFACTOR: Move to "library"?
var status = null
// Check HSTS
const subDomains = domain.split(".")
const numSubDomains = subDomains.length
var currSubDomain = ""
for (var i=1; i<=numSubDomains; i++){
// Update subdomain
currSubDomain = subDomains[numSubDomains-i] + currSubDomain
// Check against the HSTS records
if (hstsObservations[currSubDomain] !== undefined && hstsObservations[currSubDomain].content.maxAge > 0){
// TODO: take into account time of record creation
// TODO: take into account consistency
// TODO: take into account includeSubdomains directve if currSubDomain !== domain
// TODO: take into account preload list
status = "secure"
}
console.log(domain, subDomains, currSubDomain, hstsObservations[currSubDomain])
// Prepare for the next iteration
currSubDomain = "." + currSubDomain
}
// Check CSP
// TODO!
report.domains[domain].status = status
break
case "enabled":
// Nothing to do, security assessment does not affect anything
break
default:
console.log ("ERROR: Unexpected status for " + domain +" while preparing report.", report[domain])
break
}
}
console.log("Popup open: Security, sending report", report)
sendResponse(report)
break break
default: default:
console.log("Error") console.log("Error")
...@@ -78,21 +146,52 @@ function onPermissionWebRequestGranted(){ ...@@ -78,21 +146,52 @@ function onPermissionWebRequestGranted(){
/* /*
* Remembers observations about tabs (pages) * Remembers observations about tabs (pages)
* TODO: make this into a Promise
* TODO: avoid passing `request` (pick out only the necessary parts in caller)
*/ */
function rememberTabObservation(request, observation){ function rememberTabObservation(request, observations){
const tabId = request.tabId, originUrl = request.originUrl, resource = request.url, parentFrameId = request.parentFrameId const tabId = request.tabId, originUrl = request.originUrl, resource = request.url, parentFrameId = request.parentFrameId
// console.log("New observation", request)
// if no record exists, create one; if record is tied to different origin, assume it is a different page // if no record exists, create one; if record is tied to different origin, assume it is a different page
// TODO: is the above assumption correct for, e.g., SPA? // TODO: is the above assumption correct for, e.g., SPA?
const newPage = false//typeof tabObservations[tabId] === "object" && parentFrameId !== -1 && tabObservations[tabId].originUrl !== originUrl const newPage = false// parentFrameId !== -1 && tabObservations[tabId].originUrl !== originUrl
if (tabObservations[tabId] === undefined || newPage) if (tabObservations[tabId] === undefined || newPage)
tabObservations[tabId] = {originUrl: originUrl, observations:{}} tabObservations[tabId] = {originUrl: originUrl, domains:{}}
var resourceDomain = new URL(resource).hostname
if (tabObservations[tabId].domains[resourceDomain] === undefined){
// Determine whether domain is third-party
// TODO: find library or improve own implementation
const originDomain = new URL(originUrl).hostname
const thirdparty_ = thirdparty(originDomain, resourceDomain)
var resourceHost = new URL(resource).host tabObservations[tabId].domains[resourceDomain] = {thirdparty: thirdparty_, status: null, events: []}
if (tabObservations[tabId].observations[resourceHost] === undefined) }
tabObservations[tabId].observations[resourceHost] = [] tabObservations[tabId].domains[resourceDomain].events.push(observations)
tabObservations[tabId].observations[resourceHost].push(observation) for (var observation of observations){
// console.log("New observation logged", tabObservations) switch (observation.type){
case "HSTS":
// TODO: Is consistency critera good?
if (hstsObservations[resourceDomain] === undefined){
// this is first HSTS header encountered
hstsObservations[resourceDomain] = {
consistent: true,
content: observation.content,
createdTime: null // TODO: remember time
}
} else {
// this domain already has HSTS policy
if (hstsObservations[resourceDomain] && hstsObservations[resourceDomain].maxAge !== observation.content)
hstsObservations[resourceDomain].consistent = false
hstsObservations[resourceDomain].content = observation.content
hstsObservations[resourceDomain].createdTime = null // TODO: remember time
}
break
case "CSP":
break
default:
console.log("ERROR: Unhandled observation", observation)
}
}
} }
/* /*
...@@ -109,10 +208,12 @@ function onPermissionWebRequestGranted(){ ...@@ -109,10 +208,12 @@ function onPermissionWebRequestGranted(){
observations.push({type: "Cookie", content: "Cookie sent: " + details.requestHeaders[i].value}) observations.push({type: "Cookie", content: "Cookie sent: " + details.requestHeaders[i].value})
/* if (protocol === "http:" && (cookieDatabase[name] === undefined || cookieDatabase[name].secureOrigin === true)){ for (const cookieName in parsed){
console.log("LEAK", name, value, parsed) if (protocol === "http:" && (cookieDatabase[cookieName] === undefined || cookieDatabase[cookieName].secureOrigin === true)){
observations.push({type: "Cookie", content: "Cookie leak: " + cookieName + "=" + parsed[cookieName]})
}
} }
*/
break break
} }
} }
...@@ -133,7 +234,7 @@ function onPermissionWebRequestGranted(){ ...@@ -133,7 +234,7 @@ function onPermissionWebRequestGranted(){
var observations = [] var observations = []
// Get additional parameters // Get additional parameters
const protocol = new URL(details.url).protocol const url = new URL(details.url).protocol
for (var i = 0; i < details.responseHeaders.length; ++i) { for (var i = 0; i < details.responseHeaders.length; ++i) {
const headerName = details.responseHeaders[i].name.toLowerCase() const headerName = details.responseHeaders[i].name.toLowerCase()
const headerValue = details.responseHeaders[i].value const headerValue = details.responseHeaders[i].value
...@@ -141,14 +242,16 @@ function onPermissionWebRequestGranted(){ ...@@ -141,14 +242,16 @@ function onPermissionWebRequestGranted(){
case "set-cookie": case "set-cookie":
const name = headerValue.substring(0, headerValue.indexOf("=")) const name = headerValue.substring(0, headerValue.indexOf("="))
observations.push({type: "Cookie", content: "Cookie set: "+name}) observations.push({type: "Cookie", content: "Cookie set: "+name})
cookieDatabase[name] = {secureOrigin: protocol === "https:", httpOnly: true} cookieDatabase[name] = {secureOrigin: url.protocol === "https:", httpOnly: true}
break break
case "strict-transport-security": case "strict-transport-security":
// Remember HSTS header for later reference in the popup
// Parsing library options: did not find any, had to write my own
var hstsAttributes = parseResponseHeaderStrictTransportSecurity(headerValue) var hstsAttributes = parseResponseHeaderStrictTransportSecurity(headerValue)
observations.push({type: "HSTS", content: [headerValue, hstsAttributes]}) observations.push({type: "HSTS", content: hstsAttributes})
break break
// Options: did not find any, had to write my own
case "content-security-policy": case "content-security-policy":
// Options: // Options:
// https://www.npmjs.com/package/content-security-policy-parser // https://www.npmjs.com/package/content-security-policy-parser
...@@ -168,6 +271,9 @@ function onPermissionWebRequestGranted(){ ...@@ -168,6 +271,9 @@ function onPermissionWebRequestGranted(){
case "x-content-security-policy": case "x-content-security-policy":
// fallthrough // fallthrough
case "x-webkit-csp": case "x-webkit-csp":
// These are predecessors to HSTS. Although they are still supported by modern browsers, some bugs are known.
// We ignore them in desiding host security status, and instead display a warning.
// In practice, I haven't seen these ever used, so we don't waste time on them.
const message = "CSP Information: " + details.responseHeaders[i].name + " is deprecated and is known to cause problems. https://content-security-policy.com/111" const message = "CSP Information: " + details.responseHeaders[i].name + " is deprecated and is known to cause problems. https://content-security-policy.com/111"
observations.push({type: "Warning", content: message}) observations.push({type: "Warning", content: message})
break break
......
/*
* Determine if resource is a third-party based only on domain names
* @param origin - string, domain of the orgin page or frame
* @param resource - string, domain of the resource
* @returns boolean - true if thirdpary, false otherwise
*/
module.exports = {thirdparty: thirdparty}
// This is a very primitive check, need to switch to eTLD+1 check and entities list
// Currently it checks that origin domain is a suffix for the resource domain
// (after dropping common sub-domain "www" from origin)
// E.g.: "cdn.example.com".endsWith("www.example.com".replace("www.", "")) === true
function thirdparty (origin, resource){
return !resource.endsWith(origin.replace("www.", ""))
}
...@@ -23,6 +23,7 @@ h1, h2, h3, p { ...@@ -23,6 +23,7 @@ h1, h2, h3, p {
height: 100%; height: 100%;
float: left; float: left;
background-color: #eeeeee; background-color: #eeeeee;
cursor: pointer;
} }
/* Style of the Details section */ /* Style of the Details section */
...@@ -83,6 +84,7 @@ h1, h2, h3, p { ...@@ -83,6 +84,7 @@ h1, h2, h3, p {
.info{ .info{
width: 1em; width: 1em;
float: right; float: right;
cursor: help;
} }
/* /*
...@@ -120,7 +122,7 @@ h1, h2, h3, p { ...@@ -120,7 +122,7 @@ h1, h2, h3, p {
* on the left of collapsible list item * on the left of collapsible list item
* in OPEN position * in OPEN position
*/ */
.list-collapsible > li.list-collapsible--active::before { ul.list-collapsible > li.list-collapsible--active::before {
background-image: url("/includes/fontawesome/caret-down.svg"); background-image: url("/includes/fontawesome/caret-down.svg");
background-size: 20px; background-size: 20px;
display: inline-block; display: inline-block;
...@@ -128,3 +130,31 @@ h1, h2, h3, p { ...@@ -128,3 +130,31 @@ h1, h2, h3, p {
height: 20px; height: 20px;
content:""; content:"";
} }
/* The icon to the right of the domain name in the list
* Can be one of "secure", "enabled", "disabled", "insecure"
*/
li.list-domain > a::after {
background-size: 20px;
display: inline-block;
width: 20px;
height: 20px;
content:"";
}
li.list-domain--secure > a::after {
background-image: url("/includes/fontawesome/check.svg");
}
li.list-domain--enabled > a::after {
background-image: url("/includes/fontawesome/lock.svg");
}
li.list-domain--disabled > a::after {
background-image: url("/includes/fontawesome/unlock.svg");
}
li.list-domain--insecure > a::after {
background-image: url("/includes/fontawesome/exclamation-triangle.svg");
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// TODO: Move to platform // TODO: Move to platform
// List of pages on which extension is forbidden for security considerations. // List of pages on which extension is forbidden for security considerations.
// key is the URL prefix and value is the message to the user.
const restrictedPrefixes = { const restrictedPrefixes = {
"about:": "Firefox internal page.", "about:": "Firefox internal page.",
"https://addons.mozilla.org/": "Mozilla extension store (AMO) page.", "https://addons.mozilla.org/": "Mozilla extension store (AMO) page.",
...@@ -11,12 +12,14 @@ const restrictedPrefixes = { ...@@ -11,12 +12,14 @@ const restrictedPrefixes = {
const platform = chrome const platform = chrome
var tabId = null var tabId = null
var firstPartyDomain = null
platform.tabs.query({active: true, currentWindow: true }, function(activeTabs){ platform.tabs.query({active: true, currentWindow: true }, function(activeTabs){
tabId = activeTabs[0].id tabId = activeTabs[0].id
firstPartyDomain = (new URL(activeTabs[0].url)).hostname
console.log(activeTabs) console.log(activeTabs)
pageNothingToDo(activeTabs) pageNothingToDo(activeTabs[0].url)
// Notify other pages (background) that popup is open // Notify other pages (background) that popup is open
platform.runtime.sendMessage({popupOpen: true, popupSection: "general", tabId: tabId}) platform.runtime.sendMessage({popupOpen: true, popupSection: "general", tabId: tabId})
...@@ -34,10 +37,9 @@ function handleError(error) { ...@@ -34,10 +37,9 @@ function handleError(error) {
} }
*/ */
function pageNothingToDo(activeTabs){ function pageNothingToDo(url){
// Check the current page against all restricted pages // Check the current page against all restricted pages
var url = activeTabs[0].url
for (const prefix in restrictedPrefixes) for (const prefix in restrictedPrefixes)
// this is equivalent to startsWith() // this is equivalent to startsWith()
if (url.substring(0,prefix.length) === prefix){ if (url.substring(0,prefix.length) === prefix){
...@@ -89,8 +91,14 @@ function editLabel(evt) { ...@@ -89,8 +91,14 @@ function editLabel(evt) {
} }
} }
// Collapsible lists can be done with <details> and <summary>, /*
// but browser support is limited * Handle click on collapsable list label:
* show/hide details and change list label decoration
* Collapsible lists can be done with <details> and <summary>,
* but: 1. browser support is limited
* 2. still need JS toggle expanded/collapsed list decodation
* to the left of the label, aka arrow icons > or V
*/
function collapsibleList(evt){ function collapsibleList(evt){
var target = evt.target var target = evt.target
// if clicked on the label A, go level up to the LI // if clicked on the label A, go level up to the LI
...@@ -103,10 +111,52 @@ function collapsibleList(evt){ ...@@ -103,10 +111,52 @@ function collapsibleList(evt){
} }
} }
/*
*
*/
function securityList(information, editable){
function createElement(label_text, editable){
var elem = document.createElement("LI")
var label = document.createElement("A")
label.innerText = label_text
elem.appendChild(label)
// If element label is editable, create button for editing it
if (editable !== false){
var btn = document.createElement("BUTTON")
btn.innerText = "edit"
elem.appendChild(btn)
}
return elem
}
// Create list
var list = document.createElement("UL")
for (const domain in information){
// Create the list element with its details
var elem = createElement(domain, editable)
// Assign the right CSS class to display security status icon
const cssSecurityClass = "list-domain--" + information[domain].status
elem.classList.add(cssSecurityClass)
elem.classList.add("list-domain")
list.appendChild(elem)
// Create sublist of cookies
var cookie_list = document.createElement("UL")
elem.appendChild(cookie_list)
// Display sublist of cookies
// TODO: IMPLEMENT
// TODO: Display cookies for higher domains?
var entry = createElement("TODO: display cookies", editable)
cookie_list.appendChild(entry)
}
return list
}
/* /*
* Create nested list with editable labels (optional) * Create nested list with editable labels (optional)
*/ */
/*
function nestedList(information, editable){ function nestedList(information, editable){
...@@ -129,13 +179,14 @@ function nestedList(information, editable){ ...@@ -129,13 +179,14 @@ function nestedList(information, editable){
function createElement(label_text, editable){ function createElement(label_text, editable){
var elem = document.createElement("LI") var elem = document.createElement("LI")
var label = document.createElement("A") var label = document.createElement("A")
label.innerText = label_text label.innerText = JSON.stringify(label_text)
elem.appendChild(label) elem.appendChild(label)
// If element label is editable, create button for editing it // If element label is editable, create button for editing it
if (editable !== false){ if (editable !== false){
var btn = document.createElement("BUTTON") var btn = document.createElement("BUTTON")
btn.innerText = "edit" btn.innerText = "edit"
elem.appendChild(btn) elem.appendChild(btn)
elem.classList.add("list-secure")
} }
return elem return elem
} }
...@@ -160,6 +211,7 @@ function nestedList(information, editable){ ...@@ -160,6 +211,7 @@ function nestedList(information, editable){
} }
return list return list
} }
*/
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
function displaySectionPrimary(information) {} function displaySectionPrimary(information) {}
...@@ -171,14 +223,13 @@ document.addEventListener("DOMContentLoaded", function() { ...@@ -171,14 +223,13 @@ document.addEventListener("DOMContentLoaded", function() {
// TODO: avoid complete list re-creation, // TODO: avoid complete list re-creation,
// since it looses information about open and closed sublists // since it looses information about open and closed sublists
const listNew = nestedList(information.observations) const listNew = securityList(information.domains)
listNew.classList.add("list-collapsible") listNew.classList.add("list-collapsible")
listNew.addEventListener("click", editLabel) listNew.addEventListener("click", editLabel)
listNew.addEventListener("click", collapsibleList) listNew.addEventListener("click", collapsibleList)
list.replaceWith(listNew) list.replaceWith(listNew)
listNew.id = "details-" + "security" + "-list" listNew.id = "details-" + "security" + "-list"
console.log("background script sent a response:", information)
} }
function displaySectionDebugging(information) {} function displaySectionDebugging(information) {}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment