diff options
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js')
-rw-r--r-- | data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js | 488 |
1 files changed, 0 insertions, 488 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js b/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js deleted file mode 100644 index 976470b..0000000 --- a/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js +++ /dev/null @@ -1,488 +0,0 @@ -/* - * NoScript Commons Library - * Reusable building blocks for cross-browser security/privacy WebExtensions. - * Copyright (C) 2020-2024 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/>. - */ - -// depends on /nscl/common/uuid.js - -"use strict"; -/** - * Injects code into page context in a cross-browser way, providing it - * with tools to wrap/patch the DOM and the JavaScript environment - * and propagating the changes to child windows created on the fly in order - * to prevent the modifications to be cancelled by hostile code. - * - * @param {function} patchingCallback - * the (semi)privileged wrapping code to be injected. - * Warning: this is not to be considered a closure, since Chromium - * injection needs it to be reparsed out of context. - * Use the env argument to propagate parameters. - * It will be called as patchingCallback(unwrappedWindow, env). - * @param {object} env - * a JSON-serializable object to made available to patchingCallback as - * its second argument. It gets augmented by two additional properties: - * 1. a Port (port: {postMessage(), onMessage()}) object - * allowing the injected script to communicate with - * the privileged content script by calling port.postMessage(msg, [event]) - * and/or by listening to a port.onMessage(msg, event) user-defined callback. - * 2. A "xray" object property to help handling - * Firefox's XRAY wrappers. - * xray: { - * enabled: true, // false on Chromium - * unwrap(obj), // returns the XPC-wrapped object - or just obj on Chromium - * wrap(obj), // returns the XPC wrapper around the object - or just obj on Chromium - * forPage(obj), // returns cloneInto(obj) including functions and DOM objects - or just obj on Chromium - * window, // the XPC-wrapped version of unwrappedWindow, or unwrappedWindow itself on Chromium - * } - * @returns {object} port - * A Port object allowing the privileged content script to communicate - * with the injected script on the page by calling port.postMessage(msg, [event]) - * and/or by listening to a port.onMessage(msg, event) user-defined callback. - */ - -function patchWindow(patchingCallback, env = {}) { - const forcedPortId = patchingCallback.portId; - const justPort = forcedPortId && !patchingCallback.code; - const portId = forcedPortId || - this && this.portId || - `windowPatchMessages:${uuid()}`; - - const { dispatchEvent, addEventListener } = self; - - function Port(from, to) { - if (!self.document) { - // ServiceWorker scope, dummy port, won't be used. - this.postMessage = () => {}; - return; - } - // we need a double dispatching dance and maintaining a stack of - // return values / thrown errors because Chromium seals the detail object - // (on Firefox we could just append further properties to it...) - let retStack = []; - - function fire(e, detail, target = window) { - dispatchEvent.call(target, new CustomEvent(`${portId}:${e}`, {detail, composed: true})); - } - this.postMessage = function(msg, target = window) { - retStack.push({}); - let detail = {msg}; - fire(to, detail, target); - let ret = retStack.pop(); - if (ret.error) throw ret.error; - return ret.value; - }; - addEventListener.call(window, `${portId}:${from}`, event => { - if (typeof this.onMessage === "function" && event.detail) { - let ret = {}; - try { - ret.value = this.onMessage(event.detail.msg, event); - } catch (error) { - ret.error = error; - } - fire(`return:${to}`, ret); - } - }, true); - addEventListener.call(window, `${portId}:return:${from}`, event => { - let {detail} = event; - if (detail && retStack.length) { - retStack[retStack.length -1] = detail; - } - }, true); - this.onMessage = null; - } - let port = new Port("extension", "page"); - if (patchWindow.disabled) { - return port; - } - if (justPort) { - return port; - } else if (patchingCallback.code) { - patchingCallback = patchingCallback.code; - } - - const nativeExport = typeof exportFunction == "function"; - if (typeof patchingCallback !== "function") { - patchingCallback = - nativeExport ? new Function("unwrappedWindow", "env", patchingCallback) - : `function (unwrappedWindow, env) {\n${patchingCallback}\n}`; - } - if (!(nativeExport || this && this.exportFunction)) { - // Chromium - let exportFunction = (func, targetObject, {defineAs, original} = {}) => { - try { - let [propDef, getOrSet, propName] = defineAs && /^([gs]et)(?:\s+(\w+))$/.exec(defineAs) || [null, null, defineAs]; - let propDes = propName && Object.getOwnPropertyDescriptor(targetObject, propName); - if (getOrSet && !propDes) { // escalate through prototype chain - for (let proto = Object.getPrototypeOf(targetObject); proto; proto = Object.getPrototypeOf(proto)) { - propDes = Object.getOwnPropertyDescriptor(proto, propName); - if (propDes) { - targetObject = proto; - break; - } - } - } - - let toString = Function.prototype.toString; - let strVal; - if (!original) { - original = propDef && propDes ? propDes[getOrSet] : defineAs && targetObject[defineAs]; - } - if (!original) { - // It seems to be a brand new function, rather than a replacement. - // Let's ensure it appears as a native one with little hack: we proxy a Promise callback ;) - Promise.resolve(new Promise(resolve => original = resolve)); - let name = propDef && propDes ? `${getOrSet} ${propName}` : defineAs; - if (name) { - let nameDef = Reflect.getOwnPropertyDescriptor(original, "name"); - nameDef.value = name; - Reflect.defineProperty(original, "name", nameDef); - strVal = toString.call(original).replace(/^function \(\)/, `function ${name}()`) - } - } - - strVal = strVal || toString.call(original); - - let proxy = new Proxy(original, { - apply(target, thisArg, args) { - return func.apply(thisArg, args); - } - }); - - if (!exportFunction._toStringMap) { - let map = new WeakMap(); - exportFunction._toStringMap = map; - let toStringProxy = new Proxy(toString, { - apply(target, thisArg, args) { - return map.has(thisArg) ? map.get(thisArg) : Reflect.apply(target, thisArg, args); - } - }); - map.set(toStringProxy, toString.apply(toString)); - Function.prototype.toString = toStringProxy; - } - exportFunction._toStringMap.set(proxy, strVal); - - if (propName) { - if (!propDes) { - targetObject[propName] = proxy; - } else { - if (getOrSet) { - propDes[getOrSet] = proxy; - } else { - if ("value" in propDes) { - propDes.value = proxy; - } else { - return exportFunction(() => proxy, targetObject, `get ${propName}`); - } - } - Object.defineProperty(targetObject, propName, propDes); - } - } - return proxy; - } catch (e) { - console.error(e, `setting ${targetObject}.${defineAs || original}`, func); - } - return null; - }; - let cloneInto = (obj, targetObject) => { - return obj; // dummy for assignment - }; - - const code = ` - (() => { - let patchWindow = ${patchWindow}; - let cloneInto = ${cloneInto}; - let exportFunction = ${exportFunction}; - let env = ${JSON.stringify(env)}; - let portId = ${JSON.stringify(portId)}; - const console = Object.fromEntries(Object.entries(self.console).map(([n, v]) => v.bind ? [n, v.bind(self.console)] : [n,v])); - - env.port = new (${Port})("page", "extension"); - ({ - patchWindow, - exportFunction, - cloneInto, - portId, - }).patchWindow(${patchingCallback}, env); - })(); - `; - if (!self.document) { - // we're doing it with userScripts on mv3 - return {portId, code}; - } - let script = document.createElement("script"); - script.text = code; - try { - document.documentElement.insertBefore(script, document.documentElement.firstChild); - } catch(e) { - console.error(e, code); - } - script.remove(); - return port; - } - - env.port = new Port("page", "extension"); - - const {xrayEnabled} = patchWindow; - const zombieDanger = xrayEnabled && document.readyState === "complete"; - const isZombieException = e => e.message.includes("dead object"); - - const getSafeMethod = zombieDanger - ? (obj, method, wrappedObj) => { - let actualTarget = obj[method]; - return XPCNativeWrapper.unwrap(new window.Proxy(actualTarget, cloneInto({ - apply(targetFunc, thisArg, args) { - try { - return actualTarget.apply(thisArg, args); - } catch (e) { - if (isZombieException(e)) { - return (actualTarget = (wrappedObj || XPCNativeWrapper(obj))[method]).apply(thisArg, args); - } - throw e; - } - }, - }, window, {cloneFunctions: true, wrapReflectors: true} - ))); - - } : (obj, method) => obj[method]; - - const getSafeDescriptor = (proto, prop, accessor) => { - const des = Reflect.getOwnPropertyDescriptor(proto, prop); - if (zombieDanger) { - const wrappedDescriptor = Reflect.getOwnPropertyDescriptor(xray.wrap(proto), prop); - des[accessor] = getSafeMethod(des, accessor, wrappedDescriptor); - } - return des; - } - - let xrayMake = (enabled, wrap, unwrap = wrap, forPage = wrap) => ({ - enabled, wrap, unwrap, forPage, - getSafeMethod, getSafeDescriptor - }); - - let xray = !xrayEnabled - ? xrayMake(false, o => o) - : xrayMake(true, o => XPCNativeWrapper(o), o => XPCNativeWrapper.unwrap(o), - function(obj, win = this.window || window) { - return cloneInto(obj, win, {cloneFunctions: true, wrapReflectors: true}); - }); - - const patchedWindows = new WeakSet(); // track them to avoid indirect recursion - - // win: window object to modify. - function modifyWindow(win) { - try { - win = xray.unwrap(win); - env.xray = Object.assign({window: xray.wrap(win)}, xray); - - if (patchedWindows.has(win)) return; - patchedWindows.add(win); - patchingCallback(win, env); - modifyWindowOpenMethod(win); - modifyFramingElements(win); - // we don't need to modify win.opener, read skriptimaahinen notes - // at https://forums.informaction.com/viewtopic.php?p=103754#p103754 - } catch (e) { - if (e instanceof DOMException && e.name === "SecurityError") { - // In case someone tries to access SOP restricted window. - // We can just ignore this. - } else throw e; - } - } - - function modifyWindowOpenMethod(win) { - let windowOpen = win.open; - exportFunction(function(...args) { - let newWin = windowOpen.call(this, ...args); - if (newWin) modifyWindow(newWin); - return newWin; - }, win, {defineAs: "open"}); - } - - function modifyFramingElements(win) { - for (let property of ["contentWindow", "contentDocument"]) { - for (let iface of ["Frame", "IFrame", "Object"]) { - let proto = win[`HTML${iface}Element`].prototype; - modifyContentProperties(proto, property) - } - } - // auto-trigger window patching whenever new elements are added to the DOM - let patchAll = () => { - if (patchWindow.disabled) { - observer.disconnect(); - } - for (let j = 0; j in window; j++) { - try { - modifyWindow(window[j]); - } catch (e) { - console.error(e, `Patching frames[${j}]`); - } - } - }; - - let xrayWin = xray.wrap(win); - let observer = new MutationObserver(patchAll); - observer.observe(win.document, { subtree: true, childList: true }); - let patchHandler = { - apply(target, thisArg, args) { - let ret = Reflect.apply(target, thisArg, args); - const wrapped = thisArg && xray.wrap(thisArg); - if (wrapped) { - try { - if ((wrapped.ownerDocument || wrapped) === xrayWin.document) { - patchAll(); - } - } catch (e) { - console.error("Can't propagate patches (likely SOP violation).", e, thisArg, wrapped, location); // DEV_ONLY - } - } - try { - return ret ? xray.forPage(ret, win) : ret; - } catch (e) { - console.error("Can't wrap return value.", e, thisArg, target, args, ret, location); // DEV_ONLY - } - return ret; - } - }; - - let domChangers = { - Element: [ - "set innerHTML", "set outerHTML", - "after", "append", "appendChild", - "before", - "insertAdjacentElement", "insertAdjacentHTML", "insertBefore", - "prepend", - "replaceChildren", "replaceWith", "replaceChild", - "setHTML", - ], - Document: [ - "append", "prepend", "replaceChildren", - "write", "writeln", - ] - }; - - function patch(proto, method) { - let accessor; - if (method.startsWith("set ")) { - accessor = "set"; - method = method.replace("set ", ""); - } else { - accessor = "value"; - } - if (!(method in proto)) return; - while (!proto.hasOwnProperty(method)) { - proto = Object.getPrototypeOf(proto); - if (!proto) { - console.error(`Couldn't find property ${method} on the prototype chain!`); - return; - } - } - let des = getSafeDescriptor(proto, method, accessor); - des[accessor] = exportFunction(new Proxy(des[accessor], patchHandler), proto, {defineAs: `${accessor} ${method}`});; - Reflect.defineProperty(xray.unwrap(proto), method, des); - } - - for (let [obj, methods] of Object.entries(domChangers)) { - let proto = win[obj].prototype; - for (let method of methods) { - patch(proto, method); - } - } - if (patchWindow.onObject) patchWindow.onObject.add(patchAll); - } - - function modifyContentProperties(proto, property) { - let descriptor = getSafeDescriptor(proto, property, "get"); - let origGetter = descriptor.get; - let replacements = { - contentWindow() { - let win = origGetter.call(this); - if (win) modifyWindow(win); - return win; - }, - contentDocument() { - let document = origGetter.call(this); - if (document && document.defaultView) modifyWindow(document.defaultView); - return document; - } - }; - - descriptor.get = exportFunction(replacements[property], proto, {defineAs: `get ${property}`}); - Reflect.defineProperty(proto, property, descriptor); - } - - modifyWindow(window); - return port; -} - -patchWindow.xrayEnabled = typeof XPCNativeWrapper !== "undefined"; -if (patchWindow.xrayEnabled) { - // make up for object element initialization inconsistencies on Firefox - let callbacks = new Set(); - patchWindow.onObject = { - add(callback) { - callbacks.add(callback); - }, - fire() { - for (let callback of [...callbacks]) { - callback(); - } - } - }; - - const eventId = "__nscl_patchWindow_onObject__"; - const intercepted = new WeakSet(); - addEventListener(eventId, e => { - let {target} = e; - if (target instanceof HTMLObjectElement && - target.contentWindow && - !intercepted.has(target.contentWindow)) { - intercepted.add(target.contentWindow); - e.stopImmediatePropagation(); - patchWindow.onObject.fire(); - } - }, true); - - if (frameElement instanceof HTMLObjectElement) { - frameElement.dispatchEvent(new CustomEvent(eventId)); - } -} - -Object.defineProperty(patchWindow, "disabled", { - get() { - if (typeof ns === "object" && ns) { - if (ns.allows && ns.policy) { - const value = !ns.allows("script"); - Object.defineProperty(patchWindow, "disabled", { value, configurable: true }); - return value; - } - if (typeof ns.on === "function") { - ns.on("capabilities", () => { - if (ns.allows) { - this.disabled; - } - }); - } - } - return false; - }, - set(value) { - Object.defineProperty(patchWindow, "disabled", { value, configurable: true }); - return value; - }, - configurable: true, -});
\ No newline at end of file |