/** \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 . // /** * 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})();`; }