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

First minimal working prototype

Prototype displays all content origins in "Security" section.
Reworked internal storage. Display lists. Added "information" icons.
parent 1a57abf0
No related branches found
No related tags found
No related merge requests found
Showing
with 271 additions and 236 deletions
......@@ -11,14 +11,14 @@
║ Web page ║ ║ content_scripts ║ ║ Background ║
╠══════════════╣ ╠═════════════════╣ ╠═════════════════╣
║ ║ ║ inject.js ║ ║ cookiestore.js ║
║ inject.js ║<------->║ ║<----------->
║ ║ ╚═════════════════╝ ╚═════════════════╝
║ ║ |
╚══════════════╝ |
══════════════╗
║ Popup UI
══════════════╣
══════════════╝
║ inject.js ║<------->║ ║<----------->(no real page) ║
║ ║ ║ ║ ╚═════════════════╝
║ ║ ║ (no real page) |
╚══════════════╝ ╚═════════════════╝ |
╔═════════════════════╗
║ Developer tools
╠═════════════════════╣
╚═════════════════════╝
......@@ -15,4 +15,9 @@ injected code into the page (`content_scripts` and `web_accessible_resources`).
synchronous (blocking). Currently, extension blocks on `webRequest`
to maintain the smallest amount of internal datastructures. So far,
I didn't see any slowdowns. However, this is a known *tradeoff*,
which will be reversed if needed.
\ No newline at end of file
which will be reversed if needed.
### `npm` modules
- For cookie I use [`cookie`](https://www.npmjs.com/package/cookie) module.
A good alternative would be [`set-cookie-parser`]( https://www.npmjs.com/package/set-cookie-parser)
clean:
rm -rf build
firefox:
rm -rf build/firefox
mkdir build
mkdir build/firefox
cp -r source/common/* build/firefox/
cp -rf source/firefox/* build/firefox/
chromium:
rm -rf build/chromium
mkdir build
mkdir build/chromium
cp -r source/common/* build/chromium/
cp -rf source/chromium/* build/chromium/
......@@ -10,4 +10,7 @@
## Fixed?
* Options UI can be too tall (long vertically) for the provided pace. In that case, window can't resolve th right heght and scroll thing and just starts shaking. Fixed by setting body{height:700px}.
tabs is not necessary for Chrome
\ No newline at end of file
tabs is not necessary for Chrome
Automatically rebuild and reload extesnion when source changes.
https://www.reddit.com/r/webdev/comments/3rdwll/npm_makes_no_sense_to_me/
"use strict"
/*
* "Libraries" used by the background
* Ideally, these would be generic external libraries, but I didn't find them
* TODO: Separate this into a module
* TODO: Webpack https://www.reddit.com/r/webdev/comments/3rdwll/npm_makes_no_sense_to_me/
*/
const platform = require("../libraries/platform").platform
/*
* Parse HTTP Strict Transport Security response header
* @param value - string from the header
* @returns object {"max-age": non-negative int, "includeSubDomains": boolean, "preload": boolean}
*/
function parseResponseHeaderStrictTransportSecurity (headerValue){
var parsedAttributes = {"maxAge": 0, "includeSubDomains": false, "preload": false}
const attributes = headerValue.split(";")
for (var attribute of attributes){
attribute = attribute.trim().toLowerCase()
if (attribute === "")
continue
if (attribute === "includesubdomains"){
parsedAttributes.includeSubDomains = true
} else
if (attribute === "preload"){
parsedAttributes.preload = true
} else
if (attribute.startsWith("max-age")){
var value = attribute.substr(attribute.indexOf('=')+1)
if (value[0] === '"' && value.slice(-1) === '"')
value = value.slice(1, -1)
// TODO: handle errors and negative ints
parsedAttributes.maxAge = Number(value)
} else {
// ignore everything else, as per RFC 6797
console.log("ATENTION: Parsing HSTS header, unexpected attribute '" + attribute + "' in header '" + headerValue + "'")
}
}
return parsedAttributes
}
const cookie = require("cookie")
/*
* End of "Libraries"
*/
const parseResponseHeaderStrictTransportSecurity = require("../libraries/hsts/hsts").parseResponseHeaderStrictTransportSecurity
// Initialize the cookie database upon extension installation
const platform = chrome
const urlPopup = window.location.origin + "/pages/popup/popup.html"
// TODO: make an actual datasructure.
var cookieDatabase = {"example":"example"}
var cookieObservations = {}
var tabObservations = {}
/*
* Prepare datastructures of the newly installed extension
......@@ -86,8 +47,8 @@ platform.runtime.onInstalled.addListener (function() {
console.log("Popup open: General")
break
case "security":
console.log("Popup open: Security")
sendResponse(cookieDatabase)
console.log("Popup open: Security", tabObservations)
sendResponse(tabObservations[message.tab])
break
default:
console.log("Error")
......@@ -110,32 +71,49 @@ platform.runtime.onInstalled.addListener (function() {
function onPermissionWebRequestGranted(){
const urls = ["http://*/*", "https://*/*"]
/*
* Remembers observations about tabs (pages)
*/
function rememberTabObservation(request, observation){
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
// TODO: is the above assumption correct for, e.g., SPA?
const newPage = false//typeof tabObservations[tabId] === "object" && parentFrameId !== -1 && tabObservations[tabId].originUrl !== originUrl
if (tabObservations[tabId] === undefined || newPage)
tabObservations[tabId] = {originUrl: originUrl, observations:{}}
var resourceHost = new URL(resource).host
if (tabObservations[tabId].observations[resourceHost] === undefined)
tabObservations[tabId].observations[resourceHost] = []
tabObservations[tabId].observations[resourceHost].push(observation)
console.log("New observation logged", tabObservations)
}
/*
* Observe the Cookie header containing cookies being sent to the server
*/
function watchCookiesSent(details){
// console.log("Request", details)
console.log("Request")
var observations = []
const protocol = new URL(details.url).protocol
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === "Cookie") {
const cookies = details.requestHeaders[i].value.split("; ")
for (var cookie of cookies){
// Split the cookie in name and value
const index = cookie.indexOf("=")
const name = cookie.substr(0, index)
const value = cookie.substr(index + 1)
if (protocol === "http:" && (cookieDatabase[name] === undefined || cookieDatabase[name].secureOrigin === true)){
console.log("LEAK", name, value, cookie)
}
// console.log("Cookie sent: ", cookie, name, value)
// if (details.initiator.startsWith
const parsed = cookie.parse(details.requestHeaders[i].value)
observations.push({type: "Cookie", content: "Cookie sent: " + parsed})
/* if (protocol === "http:" && (cookieDatabase[name] === undefined || cookieDatabase[name].secureOrigin === true)){
console.log("LEAK", name, value, parsed)
}
// details.requestHeaders.splice(i, 1);
*/
break
}
}
rememberTabObservation(details, observations)
console.log("Request processed")
return {requestHeaders: details.requestHeaders}
}
......@@ -146,7 +124,8 @@ function onPermissionWebRequestGranted(){
// TODO: support protocols other than HTTP(S)
function watchResponse(details){
// console.log("Response", details)
console.log("Response")
var observations = []
// Get additional parameters
const protocol = new URL(details.url).protocol
......@@ -156,16 +135,13 @@ function onPermissionWebRequestGranted(){
switch (headerName){
case "set-cookie":
const name = headerValue.substring(0, headerValue.indexOf("="))
// Options:
// Better: https://www.npmjs.com/package/cookie
// Alternative: https://www.npmjs.com/package/set-cookie-parser
console.log("Cookie set: ", name)
observations.push({type: "Cookie", content: "Cookie set: "+name})
cookieDatabase[name] = {secureOrigin: protocol === "https:", httpOnly: true}
break
case "strict-transport-security":
var hstsAttributes = parseResponseHeaderStrictTransportSecurity(headerValue)
console.log("HSTS", headerValue, hstsAttributes)
observations.push({type: "HSTS", content: [headerValue, hstsAttributes]})
break
// Options: did not find any, had to write my own
case "content-security-policy":
......@@ -182,17 +158,21 @@ function onPermissionWebRequestGranted(){
// https://www.npmjs.com/package/makestatic-parse-csp
// Don't know how it works
//upgrade-insecure-requests
console.log("CSP", headerValue)
observations.push({type: "CSP", content: headerValue})
break
case "x-content-security-policy":
// fallthrough
case "x-webkit-csp":
console.log("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})
break
default:
// Header not interesting
}
}
rememberTabObservation(details, observations)
console.log ("Observations", details, observations)
console.log("Response processed")
return {responseHeaders: details.responseHeaders}
}
......@@ -212,16 +192,3 @@ function onPermissionWebRequestGranted(){
function recordCookieChange(/*object*/ changeInfo){
platform.storage.local.set({last: changeInfo.cookie, lastReason: changeInfo.cause})
}
console.log(parseResponseHeaderStrictTransportSecurity("max-age=31536000"))
console.log(parseResponseHeaderStrictTransportSecurity("max-age=15768000 ; includeSubDomains"))
console.log(parseResponseHeaderStrictTransportSecurity("max-age=\"31536000\""))
console.log(parseResponseHeaderStrictTransportSecurity("max-age=0"))
console.log(parseResponseHeaderStrictTransportSecurity("max-age=0; includeSubDomains"))
The icons in this folder were provided by
[*Font Awesome*](https://fontawesome.com/)
licensed under the Creative Commons Attribution 4.0 International license.
See details [here](https://fontawesome.com/license).
\ No newline at end of file
<svg aria-hidden="true" data-prefix="fas" data-icon="info-circle" class="svg-inline--fa fa-info-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path></svg>
\ No newline at end of file
# ``Libraries"
These are "Libraries" used by the background and parts of the extension.
Ideally, these would be generic external libraries, but I didn't find them.
const parseResponseHeaderStrictTransportSecurity = require("./hsts").parseResponseHeaderStrictTransportSecurity
// TODO: Actual test infrastructure
//
// HSTS tests adapted from RFC 6797 examples https://tools.ietf.org/html/rfc6797
//
const testParseResponseHeaderStrictTransportSecurity = [
{
"input": "max-age=31536000",
"answer": {"maxAge": 31536000, "includeSubDomains": false, "preload": false}
},
{
"input": "max-age=15768000 ; includeSubDomains",
"answer": {"maxAge": 15768000, "includeSubDomains": true, "preload": false}
},
{
"input" : "max-age=\"31536000\"",
"answer": {"maxAge": 31536000, "includeSubDomains": false, "preload": false}
},
{
"input": "max-age=0",
"answer": {"maxAge": 0, "includeSubDomains": false, "preload": false}
},
{
"input": "max-age=0; includeSubDomains",
"answer": {"maxAge": 0, "includeSubDomains": true, "preload": false}
}
]
for (const testcase of testParseResponseHeaderStrictTransportSecurity){
console.log (parseResponseHeaderStrictTransportSecurity(testcase.input))
console.log (testcase.answer)
}
\ No newline at end of file
/*
* Parse HTTP Strict Transport Security response header
* @param value - string from the header
* @returns object {"max-age": non-negative int, "includeSubDomains": boolean, "preload": boolean}
*/
module.exports = {parseResponseHeaderStrictTransportSecurity: parseResponseHeaderStrictTransportSecurity}
function parseResponseHeaderStrictTransportSecurity (headerValue){
var parsedAttributes = {"maxAge": 0, "includeSubDomains": false, "preload": false}
const attributes = headerValue.split(";")
for (var attribute of attributes){
attribute = attribute.trim().toLowerCase()
if (attribute === "")
continue
if (attribute === "includesubdomains"){
parsedAttributes.includeSubDomains = true
} else
if (attribute === "preload"){
parsedAttributes.preload = true
} else
if (attribute.startsWith("max-age")){
var value = attribute.substr(attribute.indexOf('=')+1)
if (value[0] === '"' && value.slice(-1) === '"')
value = value.slice(1, -1)
// TODO: handle errors and negative ints
parsedAttributes.maxAge = Number(value)
} else {
// ignore everything else, as per RFC 6797
console.log("ATENTION: Parsing HSTS header, unexpected attribute '" + attribute + "' in header '" + headerValue + "'")
}
}
return parsedAttributes
}
File moved
......@@ -15,15 +15,24 @@ body {
#details {
width: 50%;
float: right;
position: relative;
height: 100%;
}
.main-menu-item {
padding-left: 30px;
padding-right: 10px;
height: 33.333%;
transition: background-color 0.5s;
}
.main-menu-item-small {
height: 10%;
}
.main-menu-item-large {
height: 30%;
}
.main-menu-item--active {
background-color: #ffcc66;
transition: background-color 0.5s;
......@@ -35,6 +44,13 @@ body {
padding-top: 10px;
}
h2 {
margin: 0;
padding: 5px;
padding-top: 10px;
}
.details-item {
display: none;
}
......@@ -42,30 +58,13 @@ body {
.details-item--active {
display: block;
height: 100%;
position: absolute;
top: 0; bottom: 0;
left: 0; right: 0;
overflow: auto;
}
#main-menu-security-bar {
background-color: black;
border-radius: 13px;
/* (height of inner div) / 2 + padding */
padding: 3px;
}
#main-menu-security-bar > div {
background-color: green;
width: 40%;
/* Adjust with JavaScript */
height: 20px;
border-radius: 10px;
.info{
width: 1em;
float: right;
}
\ No newline at end of file
......@@ -9,21 +9,24 @@
<body>
<div id="main-menu">
<div id="main-menu-monitoring" class="main-menu-item">
<h2>Monitoring</h2>
<div id="main-menu-consequences" class="main-menu-item main-menu-item-small">
<h2>Cosnsequences</h2>
</div>
<div id="main-menu-sharing" class="main-menu-item">
<h2>Sharing</h2>
<div id="main-menu-monitoring" class="main-menu-item main-menu-item-large">
<h2>Monitoring<img alt="?" class="info" src="/includes/fontawesome/info-circle-solid.svg"></h2>
</div>
<div id="main-menu-security" class="main-menu-item">
<h2>Security</h2>
<div id="main-menu-security-bar">
<div></div>
</div>
<div id="main-menu-sharing" class="main-menu-item main-menu-item-large">
<h2>Sharing<img alt="?" class="info" src="/includes/fontawesome/info-circle-solid.svg"></h2>
</div>
<div id="main-menu-security" class="main-menu-item main-menu-item-large">
<h2>Security<img alt="?" class="info" src="/includes/fontawesome/info-circle-solid.svg"></h2>
</div>
</div>
<div id="details">
<div id="details-default" class="details-item details-item--active">
<div id="details-consequences" class="details-item">
<h2>You chose Consequences</h2>
</div>
<div id="details-default" class="details-item">
<h2>You didn't choose anything yet</h2>
</div>
<div id="details-monitoring" class="details-item">
......@@ -34,14 +37,30 @@
</div>
<div id="details-security" class="details-item">
<h2>You chose Security</h2>
<p>Enebling some of these options may break the site functionality. In that case, you might have to revert back.</p>
<ul id="details-security-list">
<li><a>gog-al</a> <input type="checkbox"> </li>
<li><a>gog_lc</a> <input type="checkbox"> </li>
<li><a>gog_set</a> <input type="checkbox"> </li>
<li><a>gog_us</a> <input type="checkbox"> </li>
</ul>
<button type="button">Protect all</button
<div>
<select name="site">
<option value="" disabled selected hidden>Site</option>
<option value="any">Any</option>
</select>
<select name="security">
<option value="" disabled selected hidden>Security</option>
<option value="any">Any</option>
<option value="safe">Secure by default</option>
<option value="enabled">Under protection</option>
<option value="disabled">Vulnerable</option>
<option value="insecure">Insecure</option>
</select>
<select name="severity">
<option value="" disabled selected hidden>Severity</option>
<option value="any">Any</option>
<option value="">Tempering possible</option>
<option value="volvo">Tempering detected</option>
<option value="confideniality">Snooping possible</option>
<option value="leak">Leak detected</option>
</select>
</div>
<p>Enabling some of these options may break the site functionality. In that case, you might have to revert back.</p>
<ul id="details-security-list"></ul>
</div>
</div>
......
......@@ -14,12 +14,14 @@ window.addEventListener("unload", function(evt){
const mainMenu = document.getElementById("main-menu")
const mainMenuConsequences = document.getElementById ("main-menu-consequences")
const mainMenuMonitoring = document.getElementById ("main-menu-monitoring")
const mainMenuSharing = document.getElementById ("main-menu-sharing")
const mainMenuSecurity = document.getElementById ("main-menu-security")
const detailsDefault = document.getElementById ("details-default")
const detailsConsequences = document.getElementById ("details-consequences")
const detailsMonitoring = document.getElementById ("details-monitoring")
const detailsSharing = document.getElementById ("details-sharing")
const detailsSecurity = document.getElementById ("details-security")
......@@ -42,12 +44,14 @@ document.addEventListener("DOMContentLoaded", function() {
while (elem !== undefined && elem !== null){
if (elem.classList !== undefined && elem.classList.contains("main-menu-item")){
// Found the main-menu-item
mainMenuConsequences.classList.remove("main-menu-item--active")
mainMenuMonitoring.classList.remove("main-menu-item--active")
mainMenuSharing.classList.remove("main-menu-item--active")
mainMenuSecurity.classList.remove("main-menu-item--active")
elem.classList.add("main-menu-item--active")
detailsDefault.classList.remove("details-item--active")
detailsConsequences.classList.remove("details-item--active")
detailsMonitoring.classList.remove("details-item--active")
detailsSharing.classList.remove("details-item--active")
detailsSecurity.classList.remove("details-item--active")
......@@ -63,27 +67,42 @@ document.addEventListener("DOMContentLoaded", function() {
/* Construct the "Security" details pane */
mainMenuSecurity.addEventListener("click", function(evt){
const message = {popupOpen: true, popupTab: "security"}
// TODO: live update of all information
chrome.tabs.query({active: true, currentWindow: true }, function(activeTabs){
const tabId = activeTabs[0].id
console.log(activeTabs)
const message = {popupOpen: true, popupTab: "security", tab: tabId}
function displayCookies(information) {
const list = document.getElementById("details-security-list")
// TODO: avoid complete list re-creation,
// since it looses information about open and closed sublists
// create the new list
var listNew = list.cloneNode(false)
for (const host in information.observations){
var elem = document.createElement("LI")
elem.innerHTML = host
listNew.appendChild(elem)
}
// show the new list
list.replaceWith(listNew)
console.log("background script sent a response:", information)
}
function displayCookies(cookieDatabase) {
const list = document.getElementById("details-security-list")
function handleError(error) {
console.log("Error:", error)
}
chrome.tabs.query({active: true, currentWindow: true }, function(activeTabs){
list.innerHTML = activeTabs[0].url
console.log(activeTabs)
})
console.log("background script sent a response:", cookieDatabase)
}
// Firefox:
// platform.runtime.sendMessage(message).then(displayCookies, handleError)
function handleError(error) {
console.log("Error:", error)
}
// Chrome:
platform.runtime.sendMessage(message,displayCookies)
// Firefox:
// platform.runtime.sendMessage(message).then(displayCookies, handleError)
})
// Chrome:
platform.runtime.sendMessage(message,displayCookies)
})
})
\ No newline at end of file
//
// HSTS tests adapted from RFC 6797 https://tools.ietf.org/html/rfc6797
//
var cookiestore = require("../../../source/common/background/cookiestore.js")
const testParseResponseHeaderStrictTransportSecurity = [
{
"input": "max-age=31536000",
"answer": {"max-age": 31536000, "includeSubDomains": false, "preload": false}
},
{
"input": "max-age=15768000 ; includeSubDomains",
"answer": {"max-age": 15768000, "includeSubDomains": true, "preload": false}
},
{
"input" : "max-age=\"31536000\"",
"answer": {"max-age": 31536000, "includeSubDomains": false, "preload": false}
},
{
"input": "max-age=0",
"answer": {"max-age": 0, "includeSubDomains": false, "preload": false}
},
{
"input": "max-age=0; includeSubDomains",
"answer": {"max-age": 0, "includeSubDomains": false, "preload": false}
}
]
const path = require("path")
const ncp = require("ncp").ncp
ncp.limit = 16
ncp("./source/common/pages/", "./build/pages/", function (err) {
if (err) {
return console.error(err);
}
console.log('done!');
});
ncp("./source/common/img-t/", "./build/img-t/", function (err) {
if (err) {
return console.error(err);
}
console.log('done!');
});
ncp("./source/common/includes/", "./build/includes/", function (err) {
if (err) {
return console.error(err);
}
console.log('done!');
});
ncp("./source/common/manifest.json", "./build/manifest.json", function (err) {
if (err) {
return console.error(err);
}
console.log('done!');
});
const fs = require("fs")
const base = "./source/common/"
const build = "./build/"
const dirs = ["", "pages/", "img-t/", "includes/"]
const copy = [
"pages/",
"img-t/",
"includes/",
"manifest.json"
]
const files = [
"background/cookiestore.js",
......@@ -40,23 +23,29 @@ const files = [
"web_accessible_resources/inject.js"
]
// Create all dirs if they do not exists yet
for (const dir of dirs)
if (!fs.existsSync(build+dir))
fs.mkdirSync(build+dir)
// Copy all files that do not require processing
for (const src of copy)
ncp(base + src, build + src, function (err) {
if (err)
return console.error(err)
console.log("Copied " + src)
})
// Add all files that do require processing to webpack work order
var entries = {}
for (var file of files)
for (const file of files)
entries[file] = base + file
// Ask webpack to process these files
module.exports = {
entry: entries,
output: {
path: path.resolve(__dirname, "build"),
path: path.resolve(__dirname, build),
filename: "[name]"
}
}
/*
module.exports = {
entry: "./source/common/content_scripts/inject.js",
output: {
path: path.resolve(__dirname, "build/content_scripts"),
filename: "inject.js"
}
}*/
\ No newline at end of file
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