notes/.config/chromium/Default/Extensions/doojmbjmlfjjnbmnoijecmcbfeoakpjm/11.4.6_0/xss/XSS.js

394 lines
12 KiB
JavaScript

/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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 <https://www.gnu.org/licenses/>.
*/
'use strict';
var XSS = (() => {
const ABORT = {cancel: true}, ALLOW = {};
let baseTTL = 20000; // timeout in milliseconds for each worker to perform
let workersMap = new Map();
let promptsMap = new Map();
let blockedTabs = new Map();
let requestIdCount = 0;
async function getUserResponse(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);
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: [128, 0, 0, 160]});
}
let keys = blockedTabs.get(tabId);
if (!keys) blockedTabs.set(tabId, keys = new Set());
keys.add(originKey);
return ABORT;
}
return null;
}
function doneListener(request) {
let {requestId} = request;
let worker = workersMap.get(requestId);
if (worker) {
worker.terminate();
workersMap.delete(requestId);
}
}
async function requestListener(request) {
{
let {type} = request;
if (type !== "main_frame") {
if (type === "sub_frame") type = "frame";
if (!ns.requestCan(request, type)) {
return ALLOW; // it will be blocked by RequestGuard
}
}
}
let xssReq = XSS.parseRequest(request);
if (!xssReq) return null;
let userResponse = await getUserResponse(xssReq);
if (userResponse) return userResponse;
let data;
let reasons;
try {
reasons = await XSS.maybe(xssReq);
if (!reasons) return ALLOW;
data = [];
} catch (e) {
error(e, "XSS filter processing %o", xssReq);
if (/^Timing:[^]*\binterrupted\b/.test(e.message)) {
// we don't want prompts if the request expired / errored first
return ABORT;
}
reasons = { urlInjection: true };
data = [e.toString()];
}
let prompting = (async () => {
userResponse = await getUserResponse(xssReq);
if (userResponse) return userResponse;
let {srcOrigin, destOrigin, unescapedDest} = xssReq;
let block = !!(reasons.urlInjection || reasons.postInjection)
if (reasons.protectName) {
await include("/nscl/service/ContentScriptOnce.js");
try {
await ContentScriptOnce.execute(request, {
js: [{file: "/xss/sanitizeName.js"}],
});
if (!block) return ALLOW;
} catch (e) {
error(e, "Sanitizing name in request", request.url);
}
}
if (reasons.urlInjection) data.push(`(URL) ${unescapedDest}`);
if (reasons.postInjection) data.push(`(POST) ${reasons.postInjection}`);
let source = srcOrigin && srcOrigin !== "null" ? srcOrigin : "[...]";
let {button, option} = await Prompts.prompt({
title: _("XSS_promptTitle"),
message: _("XSS_promptMessage", [source, destOrigin, data.join(",")]),
options: [
{label: _(`XSS_opt${block ? 'Block' : 'Sanitize'}`), checked: true}, // 0
{label: _("XSS_optAlwaysBlock", [source, destOrigin])}, // 1
{label: _("XSS_optAllow")}, // 2
{label: _("XSS_optAlwaysAllow", [source, destOrigin])}, // 3
],
buttons: [_("Ok")],
multiple: "focus",
width: 600,
height: 480,
});
if (button === 0 && option >= 2) {
if (option === 3) { // always allow
await XSS.setUserChoice(xssReq.originKey, "allow");
await XSS.saveUserChoices();
}
return ALLOW;
}
if (option === 1) { // always block
block = true;
await XSS.setUserChoice(xssReq.originKey, "block");
await XSS.saveUserChoices();
}
return block ? ABORT : ALLOW;
})();
promptsMap.set(xssReq.originKey, prompting);
try {
return await prompting;
} catch (e) {
error(e);
return ABORT;
}
};
function parseUrl(url) {
let u = new URL(url);
// make it cloneable
return {
href: u.href,
protocol: u.protocol,
hostname: u.hostname,
port: u.port,
origin: u.origin,
pathname: u.pathname,
search: u.search,
hash: u.hash,
};
}
return {
async start() {
if (!UA.isMozilla) return; // async webRequest is supported on Mozilla only
let {onBeforeRequest, onCompleted, onErrorOccurred} = browser.webRequest;
if (onBeforeRequest.hasListener(requestListener)) return;
await include("/legacy/Legacy.js");
await include("/xss/Exceptions.js");
this._userChoices = (await Storage.get("sync", "xssUserChoices")).xssUserChoices || {};
// convert old style whitelist if stored
let oldWhitelist = await XSS.Exceptions.getWhitelist();
if (oldWhitelist) {
for (let [destOrigin, sources] of Object.entries(oldWhitelist)) {
for (let srcOrigin of sources) {
this._userChoices[`${srcOrigin}>${destOrigin}`] = "allow";
}
}
XSS.Exceptions.setWhitelist(null);
}
let filter = {
urls: ["*://*/*"],
types: ["main_frame", "sub_frame", "object"]
};
onBeforeRequest.addListener(requestListener, filter, ["blocking", "requestBody"]);
if (!onCompleted.hasListener(doneListener)) {
onCompleted.addListener(doneListener, filter);
onErrorOccurred.addListener(doneListener, filter);
}
},
stop() {
let {onBeforeRequest} = browser.webRequest;
if (onBeforeRequest.hasListener(requestListener)) {
onBeforeRequest.removeListener(requestListener);
}
},
parseRequest(request) {
let {
url: destUrl,
originUrl: srcUrl,
method
} = request;
let destObj;
try {
destObj = parseUrl(destUrl);
} catch (e) {
error(e, "Cannot create URL object for %s", destUrl);
return null;
}
let srcObj = null;
if (srcUrl) {
try {
srcObj = parseUrl(srcUrl);
} catch (e) {}
} else {
srcUrl = "";
}
let unescapedDest = unescape(destUrl);
let srcOrigin = srcObj ? srcObj.origin : "";
if (srcOrigin === "null") {
srcOrigin = srcObj.href.replace(/[\?#].*/, '');
}
let destOrigin = destObj.origin;
let isGet = method === "GET";
return {
request,
srcUrl,
destUrl,
srcObj,
destObj,
srcOrigin,
destOrigin,
srcDomain: srcObj && srcObj.hostname && tld.getDomain(srcObj.hostname) || "",
destDomain: tld.getDomain(destObj.hostname),
originKey: `${srcOrigin}>${destOrigin}`,
unescapedDest,
isGet,
isPost: !isGet && method === "POST",
timestamp: Date.now(),
debugging: ns.local.debug,
}
},
async saveUserChoices(xssUserChoices = this._userChoices || {}) {
this._userChoices = xssUserChoices;
await Storage.set("sync", {xssUserChoices});
},
getUserChoices() {
return this._userChoices;
},
setUserChoice(originKey, choice) {
this._userChoices[originKey] = choice;
},
getUserChoice(originKey) {
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;
}
let skip = this.Exceptions.partial(xssReq);
let worker = new Worker(browser.runtime.getURL("/xss/InjectionCheckWorker.js"));
let {requestId} = xssReq.request;
workersMap.set(requestId, worker)
return await new Promise((resolve, reject) => {
worker.onmessage = e => {
let {data} = e;
if (data) {
if (data.logType) {
window[data.logType](...data.log);
return;
}
if (data.error) {
cleanup();
reject(data.error);
return;
}
}
cleanup();
resolve(e.data);
}
worker.onerror = worker.onmessageerror = e => {
cleanup();
reject(e);
}
worker.postMessage({handler: "check", xssReq, skip});
let onNavError = details => {
debug("Navigation error: %o", details);
let {tabId, frameId, url} = details;
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."));
}
};
browser.webNavigation.onErrorOccurred.addListener(onNavError,
{url: [{urlEquals: xssReq.destUrl}]});
let startTime = Date.now(), elapsed = 0, dosTimeout;
let ttlCheck = async () => {
let workersCount = workersMap.size;
if (workersCount < 1) return;
let userResponse = await getUserResponse(xssReq);
if (userResponse) return resolve(userResponse);
let now = Date.now();
elapsed += (now - startTime) / workersCount; // divide to take in account concurrency overhead
if (elapsed < baseTTL) {
startTime = now;
dosTimeout = setTimeout(ttlCheck, 2000);
return;
}
if (cleanup()) { // the request might have been aborted otherwise
reject(new Error("Timeout! DOS attack attempt?"));
} else {
debug("[XSS] Request %s already aborted while being filtered.",
xssReq.destUrl);
}
};
ttlCheck();
function cleanup() {
clearTimeout(dosTimeout);
browser.webNavigation.onErrorOccurred.removeListener(onNavError);
if (workersMap.has(requestId)) {
workersMap.delete(requestId);
worker.terminate();
return true;
}
return false;
};
});
},
async test(urlOrRequest) {
let r = {
requestId: `fake${requestIdCount++}`,
originUrl: '',
method: "GET",
};
if (typeof urlOrRequest === "string") {
r.url = urlOrRequest;
} else if (typeof urlOrRequest === "object") {
Object.assign(r, urlOrRequest);
}
return await XSS.maybe(XSS.parseRequest(r));
}
};
})();