diff --git a/src/bg/main.js b/src/bg/main.js index 7be57b7..5b9f77e 100644 --- a/src/bg/main.js +++ b/src/bg/main.js @@ -145,6 +145,7 @@ sync: ns.sync, unrestrictedTab: ns.unrestrictedTabs.has(tabId), tabId, + xssBlockedInTab: XSS.getBlockedInTab(tabId), }); }, diff --git a/src/ui/options.html b/src/ui/options.html index 51bac06..716c275 100644 --- a/src/ui/options.html +++ b/src/ui/options.html @@ -109,8 +109,9 @@ (__MSG_XssFaq__) - +
+
diff --git a/src/ui/options.js b/src/ui/options.js index 0eafab7..c99ff7f 100644 --- a/src/ui/options.js +++ b/src/ui/options.js @@ -108,16 +108,6 @@ url: a.href }); } - let button = document.querySelector("#btn-delete-xss-choices"); - let choices = UI.xssUserChoices; - button.disabled = !choices || Object.keys(choices).length === 0; - button.onclick = () => { - UI.updateSettings({ - xssUserChoices: {} - }); - button.disabled = true - }; - } opt("clearclick"); diff --git a/src/ui/popup.html b/src/ui/popup.html index 7b916d0..f64a261 100644 --- a/src/ui/popup.html +++ b/src/ui/popup.html @@ -45,6 +45,8 @@ >__MSG_OptOverrideTorBrowserPolicy____MSG_OptIncognitoPerm__
+
+
diff --git a/src/ui/ui.css b/src/ui/ui.css index 6002c39..05da66b 100644 --- a/src/ui/ui.css +++ b/src/ui/ui.css @@ -519,4 +519,26 @@ legend { .hilite-end .url { transform: none; transition: 1s transform; -} \ No newline at end of file +} + +#xssChoices { + padding: .5em; + display: none; + flex-direction: column; +} + +#xssChoices.populated { + display: flex; +} + +#xssChoices option { + background: white; +} + +#xssChoices option.block { + color: #a00; +} + +#xssChoices option.allow { + color: #080; +} diff --git a/src/ui/ui.js b/src/ui/ui.js index 216d139..b0e6fd3 100644 --- a/src/ui/ui.js +++ b/src/ui/ui.js @@ -39,6 +39,7 @@ var UI = (() => { UI.seen = m.seen; UI.unrestrictedTab = m.unrestrictedTab; UI.xssUserChoices = m.xssUserChoices; + UI.xssBlockedInTab = m.xssBlockedInTab; UI.local = m.local; UI.sync = m.sync; UI.forceIncognito = UI.incognito && !UI.sync.overrideTorBrowserPolicy; @@ -53,6 +54,7 @@ var UI = (() => { } resolve(); if (UI.onSettings) UI.onSettings(); + if (UI.tabId === -1 || UI.xssBlockedInTab) UI.createXSSChoiceManager(); await HighContrast.init(); } }); @@ -128,6 +130,61 @@ var UI = (() => { } } return input; + }, + + createXSSChoiceManager(parent = "#xssChoices") { + let choicesUI = document.querySelector(parent); + if (!choicesUI) return; + choicesUI.classList.remove("populated"); + let choices = Object.entries(UI.xssUserChoices); + let choiceKeys = UI.xssBlockedInTab; + if (choiceKeys) { + choices = choices.filter(([key,])=> choiceKeys.includes(key)); + } + if (!choices || Object.keys(choices).length === 0) { + return; + } + + choicesUI.classList.add("populated"); + + choices.sort((a, b) => { + let x = a.join("|"), y = b.join("|"); + return x < y ? -1 : x > y ? 1 : 0; + }); + let list = choicesUI.querySelector("select") || choicesUI.appendChild(document.createElement("select")); + list.size = Math.min(choices.length, 6); + list.multiple = true; + for (let o of list.options) { + list.remove(o); + } + for (let [originKey, choice] of choices) { + let [source, destOrigin] = originKey.split(">"); + let opt = document.createElement("option"); + opt.className = choice; + opt.value = originKey; + let block = choice === "block"; + opt.defaultSelected = block; + opt.text = _(`XSS_optAlways${block ? "Block" : "Allow"}`, [source || "[...]", destOrigin]); + list.add(opt); + } + let button = choicesUI.querySelector("button"); + if (!button) { + button = choicesUI.appendChild(document.createElement("button")); + button.textContent = _("XSS_clearUserChoices"); + } + button.onclick = () => { + let xssUserChoices = UI.xssUserChoices; + for (let o of list.selectedOptions) { + delete xssUserChoices[o.value]; + list.remove(o); + } + if (list.options.length === 0) { + choicesUI.classList.remove("populated"); + } + UI.updateSettings({ + xssUserChoices + }); + }; } }; diff --git a/src/xss/InjectionCheckWorker.js b/src/xss/InjectionCheckWorker.js index c78bbe9..e4d75c6 100644 --- a/src/xss/InjectionCheckWorker.js +++ b/src/xss/InjectionCheckWorker.js @@ -19,7 +19,7 @@ include("InjectionChecker.js"); let Handlers = { async check({xssReq, skip}) { - let {destUrl, unparsedRequest: request, debugging} = xssReq; + let {destUrl, request, debugging} = xssReq; let { skipParams, skipRx diff --git a/src/xss/XSS.js b/src/xss/XSS.js index 0670586..ca0f315 100644 --- a/src/xss/XSS.js +++ b/src/xss/XSS.js @@ -6,19 +6,38 @@ var XSS = (() => { let workersMap = new Map(); let promptsMap = new Map(); + let blockedTabs = new Map(); let requestIdCount = 0; async function getUserResponse(xssReq) { - let {originKey} = xssReq; + let {originKey, request} = xssReq; + let {tabId, frameId} = request; + let {browserAction} = browser; + if (frameId === 0) { + if (blockedTabs.has(tabId)) { + blockedTabs.delete(tabId); + if ("setBadgeText" in browserAction) { + browserAction.setBadgeText({tabId, text: ""}); + } + } + } await promptsMap.get(originKey); - // promptsMap.delete(originKey); + switch (await XSS.getUserChoice(originKey)) { case "allow": return ALLOW; case "block": log("Blocking request from %s to %s by previous XSS prompt user choice", xssReq.srcUrl, xssReq.destUrl); + + if ("setBadgeText" in browserAction) { + browserAction.setBadgeText({tabId, text: "XSS"}); + browserAction.setBadgeBackgroundColor({tabId, color: [0, 0, 128, 160]}); + } + let keys = blockedTabs.get(tabId); + if (!keys) blockedTabs.set(tabId, keys = new Set()); + keys.add(originKey); return ABORT; } return null; @@ -215,7 +234,7 @@ var XSS = (() => { let isGet = method === "GET"; return { - unparsedRequest: request, + request, srcUrl, destUrl, srcObj, @@ -247,6 +266,10 @@ var XSS = (() => { return this._userChoices[originKey]; }, + getBlockedInTab(tabId) { + return blockedTabs.has(tabId) ? [...blockedTabs.get(tabId)] : null; + }, + async maybe(xssReq) { // return reason or null if everything seems fine if (await this.Exceptions.shouldIgnore(xssReq)) { return null; @@ -254,7 +277,7 @@ var XSS = (() => { let skip = this.Exceptions.partial(xssReq); let worker = new Worker(browser.runtime.getURL("/xss/InjectionCheckWorker.js")); - let {requestId} = xssReq.unparsedRequest; + let {requestId} = xssReq.request; workersMap.set(requestId, worker) return await new Promise((resolve, reject) => { worker.onmessage = e => { @@ -282,7 +305,7 @@ var XSS = (() => { let onNavError = details => { debug("Navigation error: %o", details); let {tabId, frameId, url} = details; - let r = xssReq.unparsedRequest; + let r = xssReq.request; if (tabId === r.tabId && frameId === r.frameId) { cleanup(); reject(new Error("Timing: request interrupted while being filtered, no need to go on."));