diff options
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/code_builders.js')
-rw-r--r-- | data/extensions/jsr@javascriptrestrictor/code_builders.js | 664 |
1 files changed, 664 insertions, 0 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/code_builders.js b/data/extensions/jsr@javascriptrestrictor/code_builders.js new file mode 100644 index 0000000..8173c43 --- /dev/null +++ b/data/extensions/jsr@javascriptrestrictor/code_builders.js @@ -0,0 +1,664 @@ +/** \file + * \brief Functions that build code that modifies JS evironment provided to page scripts + * + * \author Copyright (C) 2019 Libor Polcak + * \author Copyright (C) 2021 Giorgio Maone + * \author Copyright (C) 2022 Marek Salon + * + * \license 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/>. +// + +/** + * Create IIFE to wrap the code in closure + */ +function enclose_wrapping(code, ...args) { + return `try{(function(...args) {${code}})(${args});} catch (e) {console.error(e)}`; +} + +/** + * Create wrapping that might be IIFE or a function that is immediately called and also available + * for future. + */ +function enclose_wrapping2(code, name, params, call_with_window) { + if (name === undefined) { + return enclose_wrapping(code); + } + return `function ${name}(${params}) {${code}} + ${name}(${call_with_window ? "window" : ""});` +} + +/** + * Create code containing call of API counting function. + */ +function create_counter_call(wrapper, type) { + let {parent_object, parent_object_property} = wrapper; + let updateCount = `${parent_object}.${parent_object_property}`; + + if ("update_count" in wrapper) { + if (typeof wrapper.update_count === "string") updateCount = wrapper.update_count; + } + + return updateCount ? `if (fp_enabled && fp_${type}_count < 1000) { + updateCount(${JSON.stringify(updateCount)}, "${type}", args.map(x => JSON.stringify(x))); + fp_${type}_count += 1; + }` : ""; +} + +/** + * This function create code (as string) that creates code that can be used to inject (or overwrite) + * a function in the page context. + */ +function define_page_context_function(wrapper) { + let {parent_object, parent_object_property, original_function, replace_original_function} = wrapper; + if (replace_original_function) { + let lastDot = original_function.lastIndexOf("."); + parent_object = original_function.substring(0, lastDot); + parent_object_property = original_function.substring(lastDot + 1); + } + let originalF = original_function || `${parent_object}.${parent_object_property}`; + let code = ` + let originalF = ${originalF}; + var fp_call_count = 0; + let replacementF = function(${wrapper.wrapping_function_args}) { + try { + let args = Array.prototype.slice.apply(arguments); + ${create_counter_call(wrapper, "call")} + } + catch (e) { /* No action: let the wrapper continue uninterupted. TODO: let the user decide? */ }` + + // if apply_if condition is present, we need to wrap for FPD anyhow + if (wrapper.apply_if !== undefined) { + code += ` + if (${wrapper.apply_if}) { + ${wrapper.wrapping_function_body} + } + else { + return originalF.call(this, ${wrapper.wrapping_function_args}); + }` + } + else { + code += `${wrapper.wrapping_function_body}` + } + + code += ` + }; + if (WrapHelper.XRAY) { + let innerF = replacementF; + replacementF = function(...args) { + + // prepare callbacks + args = args.map(a => typeof a === "function" ? WrapHelper.pageAPI(a) : a); + + let ret = WrapHelper.forPage(innerF.call(this, ...args)); + if (ret) { + if (ret instanceof xrayWindow.Promise || ret instanceof WrapHelper.unX(xrayWindow).Promise) { + ret = Promise.resolve(ret); + } + try { + ret = WrapHelper.unX(ret); + } catch (e) {} + } + return ret; + } + } + exportFunction(replacementF, ${parent_object}, {defineAs: '${parent_object_property}'}); + ${wrapper.post_replacement_code || ''}` + + return enclose_wrapping2(code, wrapper.wrapping_code_function_name, wrapper.wrapping_code_function_params, wrapper.wrapping_code_function_call_window); +} + +/** + * This function creates code that assigns an already defined function to given property. + */ +function generate_assign_function_code(code_spec_obj) { + return `exportFunction(${code_spec_obj.export_function_name}, + ${code_spec_obj.parent_object}, + {defineAs: '${code_spec_obj.parent_object_property}'}); + `; +} + +/** + * This function wraps object properties using WrapHelper.defineProperties(). + */ +function generate_object_properties(code_spec_obj, fpd_only) { + var code = ` + if (!("${code_spec_obj.parent_object_property}" in ${code_spec_obj.parent_object})) { + // Do not wrap an object that is not defined, e.g. because it is experimental feature. + // This should reduce fingerprintability. + return; + } + `; + for (let assign of code_spec_obj.wrapped_objects || []) { + code += `var ${assign.wrapped_name} = window.${assign.original_name};`; + } + code += ` + { + let obj = ${code_spec_obj.parent_object}; + let prop = "${code_spec_obj.parent_object_property}"; + let descriptor = Object.getOwnPropertyDescriptor(obj, prop); + if (!descriptor) { + // let's traverse the prototype chain in search of this property + for (let proto = Object.getPrototypeOf(obj); proto; proto = Object.getPrototypeOf(obj)) { + if (descriptor = Object.getOwnPropertyDescriptor(proto, prop)) { + obj = WrapHelper.unX(obj); + break; + } + } + if (!descriptor) descriptor = { + // Originally not a descriptor, fallback + enumerable: true, + configurable: true, + }; + } + ` + for (let wrap_spec of code_spec_obj.wrapped_properties) { + // variable name used for distinguishing between different original properties of the same wrapper + var original_property = `originalP_${wrap_spec.property_name}`; + + var counting_wrapper = ` + function(...args) { + ${create_counter_call(code_spec_obj, wrap_spec.property_name)} + + // checks type of underlying wrapper/definition and returns it (no changes to semantics) + if (typeof (${fpd_only ? original_property : wrap_spec.property_value}) === 'function') { + return (${fpd_only ? original_property : wrap_spec.property_value}).bind(this)(...args); + } + else { + return (${fpd_only ? original_property : wrap_spec.property_value}); + } + } + `; + + if (fpd_only) { + code += `var ${original_property} = descriptor["${wrap_spec.property_name}"];`; + } + + code += ` + originalPDF = descriptor["${wrap_spec.property_name}"]; + var fp_${wrap_spec.property_name}_count = 0; + replacementPD = ${counting_wrapper}; + descriptor["${wrap_spec.property_name}"] = replacementPD; + `; + } + code += `WrapHelper.defineProperty(${code_spec_obj.parent_object}, + "${code_spec_obj.parent_object_property}", descriptor); + }`; + return code; +} + +/** + * This function removes a property. + */ +function generate_delete_properties(code_spec_obj) { + var code = ` + `; + for (prop of code_spec_obj.delete_properties) { + code += ` + if ("${prop}" in ${code_spec_obj.parent_object}) { + // Delete only properties that are available. + // The if should be safe to be deleted but it can possibly reduce fingerprintability + WrapHelper.defineProperty( + ${code_spec_obj.parent_object}, + "${prop}", {get: undefined, set: undefined, configurable: false, enumerable: false} + ); + } + ` + } + return code; +} + +/** + * This function generates code that makes an assignment. + */ +function generate_assignement(code_spec_obj) { + return `${code_spec_obj.parent_object}.${code_spec_obj.parent_object_property} = ${code_spec_obj.value};` +} + +/** + * This function builds the wrapping code. + */ +var build_code = function(wrapper, ...args) { + let post_wrapping_functions = { + function_define: define_page_context_function, + function_export: generate_assign_function_code, + object_properties: generate_object_properties, + delete_properties: generate_delete_properties, + assign: generate_assignement, + }; + + let target = `${wrapper.parent_object}.${wrapper.parent_object_property}`; + let code = ""; + + { + // Do not wrap an object that is not defined, e.g. because it is experimental feature. + // This should reduce fingerprintability. + let objPath = [], undefChecks = []; + for (leaf of target.split('.')) { + undefChecks.push( + objPath.length ? `!("${leaf}" in ${objPath.join('.')})` // avoids e.g. Event.prototype.timeStamp from throwing "Illegal invocation" + : `typeof ${leaf} === "undefined"` + ); + objPath.push(leaf); + } + + code += ` + try { + if (${undefChecks.join(" || ")}) return; + } catch (e) { + return; + }`; + } + + for (let {original_name = target, wrapped_name, callable_name} of wrapper.wrapped_objects || []) { + if (original_name !== target) { + code += ` + if (typeof ${original_name} === undefined) return; + `; + } + if (wrapped_name) { + code += `var ${wrapped_name} = window.${original_name};`; + } + if (callable_name) { + code += `var ${callable_name} = WrapHelper.pageAPI(window.${original_name});`; + } + } + code += ` + ${wrapper.helping_code || ''}`; + + if (wrapper.wrapping_function_body){ + code += `${define_page_context_function(wrapper)}`; + } + + let build_post_normal = () => { + if (wrapper["post_wrapping_code"] !== undefined) { + for (code_spec of wrapper["post_wrapping_code"]) { + if (code_spec.apply_if !== undefined) { + code += `if (${code_spec.apply_if}) {` + } + code += post_wrapping_functions[code_spec.code_type](code_spec); + if (code_spec.apply_if !== undefined) { + code += "}"; + } + // if not wrapped because of apply_if condition in post wrapping object, still needs to be wrapped for FPD + if (code_spec.apply_if !== undefined && code_spec.code_type == "object_properties") { + code += "else {" + generate_object_properties(code_spec, true) + "}"; + } + } + } + } + + let build_post_fpd = () => { + if (wrapper["post_wrapping_code"] !== undefined) { + for (code_spec of wrapper["post_wrapping_code"]) { + // if not wrapped because of apply_if condition in post wrapping object, still needs to be wrapped for FPD + if (code_spec.apply_if !== undefined && code_spec.code_type == "object_properties") { + code += generate_object_properties(code_spec, true); + } + } + } + } + + // if apply_if is present in main wrapper object and contains post wrapping code -> wrap for FPD only if condition is FALSE + if (wrapper.apply_if !== undefined) { + code += `if (${wrapper.apply_if}) {` + build_post_normal(); + code += `} else {` + build_post_fpd(); + code += `}` + } + else { + build_post_normal(); + } + + if (wrapper["wrapper_prototype"] !== undefined) { + let source = wrapper.wrapper_prototype; + code += `if (${target.prototype} !== ${source.prototype}) { // prevent cyclic __proto__ errors on Proxy + Object.setPrototypeOf(${target}, ${source}); + }`; + } + code += ` + if (${wrapper.freeze}) { + Object.freeze(${wrapper.parent_object}.${wrapper.parent_object_property}); + } + `; + + return enclose_wrapping(code, ...args); +}; + +/** + * Transform wrapping arrays into injectable code. + */ +function wrap_code(wrappers) { + if (wrappers.length === 0) { + return; // Nothing to wrap + } + + let build = (wrapper) => { + try { + return build_code(build_wrapping_code[wrapper[0]], wrapper.slice(1)); + } catch (e) { + console.error(e); + return ""; + } + }; + + let fpd_placeholder = "\n\n// FPD_S\n\n// FPD_E" + return generate_code(joinWrappingCode(wrappers.map(x => build(x))) + fpd_placeholder); +} + +/** + * Join array of wrapping codes into single string. + */ +let joinWrappingCode = code => { + return code.join("\n").replace(/\bObject\.(create|definePropert)/g, "WrapHelper.$1"); +} + +/** + * Append wrapped codes to NSCL helpers and create injectable code. + */ +function generate_code(wrapped_code) { + let code = (w => { + + // cross-wrapper globals + let xrayWindow = window; // the "privileged" xray window wrapper in Firefox + { + let {port} = env; + function updateCount(wrapperName, wrapperType, wrapperArgs) { + port.postMessage({ + wrapperName, + wrapperType, + wrapperArgs + }); + } + } + let WrapHelper; // xray boundary helper + { + const XRAY = (xrayWindow.top !== unwrappedWindow.top && typeof XPCNativeWrapper !== "undefined"); + let privilegedToPage = new WeakMap(); + let pageReady = new WeakSet(); + + let promise = obj => obj.then(r => forPage(r)); + + forPage = obj => { + if (typeof obj !== "object" && typeof obj !== "function" || obj === null + || pageReady.has(obj)) return obj; + if (privilegedToPage.has(obj)) return privilegedToPage.get(obj); // keep clone identity + let ret = obj; // fallback + if (XRAY) { + if (obj instanceof xrayWindow.Promise) { + return promise(obj); + } + if (obj instanceof unX(xrayWindow).Promise) { + return new xrayWindow.Promise((resolve, reject) => { + unX(xrayWindow).Promise.prototype.then.call(obj, + forPage(r => { + if (r.wrappedJSObject && r.wrappedJSObject === unX(r)) { + r = unX(r) + } else r = forPage(r); + resolve(r); + } + ), forPage(e => reject(e))) + }); + } + try { + if (obj.wrappedJSObject && obj.wrappedJSObject === unX(obj)) { + return obj; + } + } catch (e) {} + try { + ret = cloneInto(obj, unX(xrayWindow), {cloneFunctions: true, wrapReflectors: true}); + } catch (e) { + // can't be cloned: must be a Proxy + } + } else { + // Chromium: just use patchWindow's exportFunction() to make our wrappers look like native functions + if (typeof obj === "function") { + ret = exportFunction(obj, unX(xrayWindow)); + } + } + pageReady.add(ret); + privilegedToPage.set(obj, ret); + return ret; + } + + let fixProp = (d, prop, obj) => { + for (let accessor of ["set", "get"]) { + if (typeof d[accessor] === "function") { + let f = d[accessor]; + d[accessor] = exportFunction(d[accessor], obj, {defineAs: `${accessor} ${prop}`}); + } + } + if (typeof d.value === "object") d.value = forPage(d.value); + return d; + }; + let OriginalProxy = unwrappedWindow.Proxy; + let Proxy = OriginalProxy; + let pageAPI, unX; + if (XRAY) { + + unX = o => XPCNativeWrapper.unwrap(o); + + // automatically export Proxy constructor parameters + let proxyConstructorHandler = forPage({ + construct(targetConstructor, args) { + let [target, handler] = unX(args); + let selfProxy = !!(target === WrapHelper.Proxy && handler.construct); + if (selfProxy) { + let {construct} = handler; + handler.construct = (target, args) => { + let proxy = construct(target, unX(args)); + pageReady.add(proxy); + return proxy; + } + } + + target = forPage(target); + handler = forPage(handler); + let proxy = new targetConstructor(target, handler); + pageReady.add(proxy); + return proxy; + }, + }); + Proxy = new OriginalProxy(OriginalProxy, proxyConstructorHandler); + let then; + let apiHandler = { + apply(target, thisArg, args) { + let pa = unX(args); + for (let j = pa.length; j-- > 0;) { + let a = pa[j]; + if (a && unX(a) === a) { + pa[j] = forPage(a); + } else if (typeof a === "function") { + pa[j] = new Proxy(a, apiHandler); + } + } + let ret = target.apply(thisArg, pa); + if (ret) { + if (ret instanceof xrayWindow.Promise) { + then = then || (then = new Proxy(xrayWindow.Promise.prototype.then, apiHandler)); + if (ret.wrappedJSObject) { + let p = unX(ret); + if (p === ret.wrappedJSObject) { + p.then = then + ret = p; + } + } + } else { + ret = forPage(ret); + } + } + return ret; + } + }; + + + + pageAPI = f => { + if (typeof f !== "function") return f; + return new Proxy(f, apiHandler); + } + } else { + pageAPI = unX = f => f; + } + + let overlay; + { + let overlayProtos = new WeakMap(); + let overlayObjects = new WeakMap(); + overlay = (obj, data) => { + obj = unX(obj); + let proto = obj.__proto__; + let proxiedProps = overlayProtos.get(proto); + if (!proxiedProps) overlayProtos.set(proto, proxiedProps = {}); + let props = Object.getOwnPropertyDescriptors(data); + for (let p in props) { + if (p in proxiedProps) continue; + for (let rootProto = proto; ;) { + let protoProps = Object.getOwnPropertyDescriptors(rootProto); + let protoProp = protoProps[p]; + if (!protoProp) { + rootProto = rootProto.__proto__; + if (rootProto) continue; + } + if (protoProp) { + let original; + if (protoProp.get) { + let getterHandler = forPage({ + apply(target, thisArg, args) { + let obj = unX(thisArg); + if (overlayObjects.has(obj)) { + let data = overlayObjects.get(obj); + return forPage(data[p]); + } + return target.apply(thisArg, args); + } + }); + let original = protoProp.get; + protoProp.get = new Proxy(protoProp.get, getterHandler); + } else if (typeof protoProp.value === "function") { + original = protoProp.value; + let methodHandler = forPage({ + apply(target, thisArg, args) { + let obj = unX(thisArg); + if (overlayObjects.has(obj)) { + let data = overlayObjects.get(obj); + return forPage(data[p].apply(thisArg, args)); + } + return target.apply(thisArg, args); + } + }); + protoProp.value = new Proxy(protoProp.value, methodHandler); + } else { + protoProp = null; + } + if (protoProp) { + Reflect.defineProperty(rootProto, p, protoProp); + proxiedProps[p] = {rootProto, original, protoProp}; + break; + } + } + Reflect.defineProperty(obj, p, forPage(props[p])); + break; + } + } + overlayObjects.set(obj, data); + return obj; + } + } + let createObj = unX(xrayWindow).Object.create; + WrapHelper = { + XRAY, // boolean, are we in a xray environment (i.e. on Firefox)? + shared: {}, // shared storage object for in inter-wrapper coordination + + // WrapHelper.forPage() can be used by "complex" proxies to explicitly + // prepare an object/function created in Firefox's sandboxed content script environment + // to be consumed/called from the page context, and to make replacements for native + // objects and functions provided by the wrappers look as much native as possible. + // in most cases, however, this gets automated by the code builders replacing + // Object methods found in the wrapper sources with their WrapHelper counterparts + // and by proxying "callable_name" functions through WrapHelper.pageAPI(). + forPage, + _forPage: x => x, // dummy for easily testing out the preparation + isForPage: obj => pageReady.has(obj), + unX, // safely waives xray wrappers + // xray-aware Object creation helpers, mostly used transparently by the code builders + defineProperty(obj, prop, descriptor, ...args) { + obj = unX(obj); + return Object.defineProperty(obj, prop, fixProp(descriptor, prop, obj), ...args); + }, + defineProperties(obj, descriptors, ...args) { + obj = unX(obj); + for (let [prop, d] of Object.entries(descriptors)) { + descriptors[prop] = fixProp(d, prop, obj); + } + return Object.defineProperties(obj, descriptors, ...args); + }, + create(proto, descriptors) { + let unwrappedProto = unX(proto); + let obj = unX(createObj(unwrappedProto)); + try { + if (proto && !obj.__proto__) { + obj = Object.create(XPCNativeWrapper(proto)); + } + } catch (e) { + // access denied to obj.__proto__, wrappers mismatch + obj = forPage(Object.create(proto)); + } + return descriptors ? this.defineProperties(obj, descriptors) && obj : obj; + }, + + // WrapHelper.overlay(obj, data) + // Proxies the prototype of the obj object in order to return the properties of the data object + // as if they were native properties (e.g. as if they were returned by getters on the prototype chain, + // rather than defined on the instance). + // This allows spoofing some native objects data in a less detectable / fingerprintable way than using + // Object.defineProperty(). See wrappingS-MCS.js for an example. + overlay, + // WrapHelper.pageAPI(f) + // Proxies the function/method f so that arguments and return values, and especially callbacks and + // Promise objects, are recursively managed in order to transparently marshal objects back + // and forth Firefox's sandbox for extensions and the page scripts. + pageAPI, + // the original Proxy constructor + OriginalProxy, + // our xray-aware proxied Proxy constructor + Proxy, + }; + Object.freeze(WrapHelper); + } + + with(unwrappedWindow) { + let window = unwrappedWindow; + let {Proxy} = WrapHelper; + let {Promise, Object, Array, JSON} = xrayWindow; + + // add flag variable that determines whether messages should be sent + let fp_enabled = false; + + try { + // WRAPPERS // + } finally { + // cleanup environment if necessary + } + + // after injection code completed, allow messages (calls from wrappers won't be counted) + fp_enabled = true; + } + }).toString().replace('// WRAPPERS //', wrapped_code) + + return `(${code})();`; +} + |