trisquel-icecat/icecat/extensions/gnu/jsr@javascriptrestrictor/code_builders.js

759 lines
25 KiB
JavaScript

/** \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
* \author Copyright (C) 2023 Martin Zmitko
*
* \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 resource = `${parent_object}.${parent_object_property}`;
let args = wrapper.report_args ? "args.map(x => JSON.stringify(x))" : "[]"
return `if (fp_enabled && fp_${type}_count < 1000) {
var stack = undefined;
if (fpdTrackCallers) {
try {
throw new Error("FPDCallerTracker");
} catch (e) {
stack = e.stack.toString();
}
}
updateCount(${JSON.stringify(resource)}, "${type}", ${args}, stack);
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 {
${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 (typeof browser_polyfill_used === "undefined") {
code += `
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;
};
`;
}
code += `
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 descriptor = WrapHelper.getDescriptor(${code_spec_obj.parent_object}, "${code_spec_obj.parent_object_property}");
`
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});
}`;
}
if (wrapper.freeze !== undefined) {
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");
}
/**
* Insert WebAssembly initialization code into wrapped injection code.
*/
function insert_wasm_code(code) {
let wasm_code = (() => {
const wasm_memory = new WebAssembly.Memory({initial: 1});
// Memory layout:
// +-----------------+--------------+----------+---------------------------------------- - -
// | CRC table | Xoring table | Reserved | Data
// | 256 * u16 | 8 * u32 | |
// +-----------------+--------------+----------+---------------------------------------- - -
// 0 512 544 1024
const crc_offset = 0;
const xoring_offset = 512;
const reserved_offset = 544;
const data_offset = 1024;
WebAssembly.instantiateStreaming(fetch("/* WASM_URL */"), {env: {memory: wasm_memory}}).then(result => {
new Uint16Array(wasm_memory.buffer, crc_offset, crc16_table.length).set(crc16_table);
const xoring = new Uint32Array(wasm_memory.buffer, xoring_offset, 8);
for (let i = 0; i < 64; i += 8) {
xoring[i / 8] = parseInt(domainHash.slice(i, i + 8), 16) >>> 0;
}
wasm = {
// Getter and setter for data in WASM memory. Because we need access from page context,
// we can't just get a memory view and use it directly. The view needs to be exported to be
// usable in the page context on Firefox.
// This means that views returned by wasm.get() aren't actually views of the WASM memory,
// but rather copies of the data and modifying them won't affect the underlying memory.
// We can't export the wasm memory directly either, it is bound to the WASM instance and
// it's not possible to export the instance as well.
// For constructing views of the correct type, we can't use constructors passed from page context,
// a content script constructor must be used. For now, we use just 2 types, so passing a bool
// to differentiate is enough.
get(length, offset = 0, float = false) {
if (float) {
return WrapHelper.forPage(new Float32Array(wasm_memory.buffer, data_offset + offset, length));
} else {
return WrapHelper.forPage(new Uint8Array(wasm_memory.buffer, data_offset + offset, length));
}
},
set(data, offset = 0, float = false) {
if (float) {
new Float32Array(wasm_memory.buffer, data_offset + offset, data.length).set(data);
} else {
new Uint8Array(wasm_memory.buffer, data_offset + offset, data.length).set(data);
}
},
// Grow the WASM memory if needed.
grow(needed_bytes) {
const memory_size = wasm_memory.buffer.byteLength;
needed_bytes += data_offset;
if (memory_size < needed_bytes) {
try {
wasm_memory.grow(Math.ceil((needed_bytes - memory_size) / 65536));
} catch (e) {
console.warn("Failed to grow WASM memory, falling back to JS implementation", e);
return false;
}
}
return true;
},
// Make WASM exported functions available to wrappers.
...result.instance.exports,
ready: true
}
Object.freeze(wasm);
}).catch(e => {
console.warn("Failed to instantiate WASM farbling module, falling back to JS implementation", e);
});
}).toString().replace("/* WASM_URL */", browser.runtime.getURL("farble.wasm"));
return code.replace("// WASM_CODE //", `(${wasm_code})()`);
}
/**
* 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 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;
},
getDescriptor(obj, prop) {
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 = unX(obj);
break;
}
}
if (!descriptor) descriptor = {
// Originally not a descriptor, fallback
enumerable: true,
configurable: true,
};
}
return descriptor;
},
// 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);
}
// The object available to wrappers later containing farbling WASM optmitimized functions if enabled
let wasm = Object.freeze({ready: false});
// Farbling WebAssembly module initialization placeholder
// WASM_CODE //
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;
(function () {
let {port} = env;
function updateCount(wrapperName, wrapperType, wrapperArgs, stack) {
port.postMessage({
wrapperName,
wrapperType,
wrapperArgs,
stack
});
}
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})();`;
}