From 3d98b18759822066b3073f25ab565f94244b04ab Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Mon, 16 May 2022 21:09:59 +0200 Subject: [PATCH 01/16] Add stealth extension --- .dockerignore | 1 + Extension/.gitignore | 1 + .../src/background/javascript-instrument.ts | 13 +- Extension/src/feature.ts | 18 +- Extension/src/stealth/error.ts | 132 ++++ Extension/src/stealth/instrument.ts | 742 ++++++++++++++++++ Extension/src/stealth/settings.ts | 242 ++++++ Extension/src/stealth/stealth.ts | 381 +++++++++ Extension/webpack.config.js | 2 + LICENSE | 380 +++++++++ crawler.py | 3 + docs/Configuration.md | 11 +- openwpm/config.py | 1 + 13 files changed, 1912 insertions(+), 15 deletions(-) create mode 100644 Extension/src/stealth/error.ts create mode 100644 Extension/src/stealth/instrument.ts create mode 100644 Extension/src/stealth/settings.ts create mode 100644 Extension/src/stealth/stealth.ts diff --git a/.dockerignore b/.dockerignore index ab77e3fc3..a2e5aa6f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,4 +26,5 @@ Extension/dist Extension/openwpm.xpi Extension/src/content.js Extension/src/feature.js +Extension/src/stealth.js Extension/build diff --git a/Extension/.gitignore b/Extension/.gitignore index a1ff4b938..5e48af9a8 100644 --- a/Extension/.gitignore +++ b/Extension/.gitignore @@ -15,3 +15,4 @@ dist openwpm.xpi bundled/content.js bundled/feature.js +bundled/stealth.js diff --git a/Extension/src/background/javascript-instrument.ts b/Extension/src/background/javascript-instrument.ts index ffb02b80d..1e450ac12 100644 --- a/Extension/src/background/javascript-instrument.ts +++ b/Extension/src/background/javascript-instrument.ts @@ -47,11 +47,13 @@ export class JavascriptInstrument { private readonly dataReceiver; private onMessageListener; private configured: boolean = false; + private legacy: boolean = true; private pendingRecords: JavascriptOperation[] = []; private crawlID; - constructor(dataReceiver) { + constructor(dataReceiver, legacy) { this.dataReceiver = dataReceiver; + this.legacy = legacy; } /** @@ -115,14 +117,14 @@ export class JavascriptInstrument { } public async registerContentScript( - testing: boolean, - jsInstrumentationSettings: JSInstrumentRequest[], + testing?: true, + jsInstrumentationSettings?: JSInstrumentRequest[], ) { const contentScriptConfig = { testing, jsInstrumentationSettings, }; - if (contentScriptConfig) { + if (contentScriptConfig && this.legacy) { // TODO: Avoid using window to pass the content script config await browser.contentScripts.register({ js: [ @@ -138,8 +140,9 @@ export class JavascriptInstrument { matchAboutBlank: true, }); } + const entryScript = (this.legacy) ? "/content.js" : "/stealth.js"; return browser.contentScripts.register({ - js: [{ file: "/content.js" }], + js: [{ file: entryScript }], matches: [""], allFrames: true, runAt: "document_start", diff --git a/Extension/src/feature.ts b/Extension/src/feature.ts index e972671b5..c13b11113 100644 --- a/Extension/src/feature.ts +++ b/Extension/src/feature.ts @@ -21,6 +21,7 @@ async function main() { navigation_instrument: true, cookie_instrument: true, js_instrument: true, + stealth_js_instrument:false, cleaned_js_instrument_settings: [ { object: `window.CanvasRenderingContext2D.prototype`, @@ -72,15 +73,16 @@ async function main() { const cookieInstrument = new CookieInstrument(loggingDB); cookieInstrument.run(config.browser_id); } - - if (config.js_instrument) { + if (config['stealth_js_instrument']) { + loggingDB.logDebug("Stealth JavaScript Instrumentation enabled"); + let stealthJSInstrument = new JavascriptInstrument(loggingDB, false); + stealthJSInstrument.run(config['browser_id']); + await stealthJSInstrument.registerContentScript(); + } if (config['js_instrument']) { loggingDB.logDebug("Javascript instrumentation enabled"); - const jsInstrument = new JavascriptInstrument(loggingDB); - jsInstrument.run(config.browser_id); - await jsInstrument.registerContentScript( - config.testing, - config.cleaned_js_instrument_settings, - ); + let jsInstrument = new JavascriptInstrument(loggingDB, true); + jsInstrument.run(config['browser_id']); + await jsInstrument.registerContentScript(config['testing'], config['cleaned_js_instrument_settings']); } if (config.http_instrument) { diff --git a/Extension/src/stealth/error.ts b/Extension/src/stealth/error.ts new file mode 100644 index 000000000..d45aa1424 --- /dev/null +++ b/Extension/src/stealth/error.ts @@ -0,0 +1,132 @@ +/* + * Functionality to generate error objects + */ +function generateErrorObject(err, context){ + // TODO: Pass context + context = (context !== undefined) ? context : window; + const cleaned = cleanErrorStack(err.stack) + const stack = splitStack(cleaned); + const lineInfo = getLineInfo(stack); + const fileName = getFileName(stack); + let fakeError; + try{ + // fake type, message, filename, column and line + const propertyName = "stack"; + fakeError = new context.wrappedJSObject[err.name](err.message, fileName); + fakeError.lineNumber = lineInfo.lineNumber; + fakeError.columnNumber = lineInfo.columnNumber; + }catch(error){ + console.log("ERROR creation failed. Error was:" + error); + } + return fakeError; +} + +/* + * Trims traces from the stack, which contain the extionsion ID + */ +function cleanErrorStack(stack) { + const extensionID = browser.runtime.getURL(""); + const lines = (typeof(stack) !== "string") ? stack : splitStack(stack); + lines.forEach(line =>{ + if (line.includes(extensionID)){ + stack = stack.replace(line+"\n",""); + } + }); + return stack; +} + +/* + * Provides the index the first call outside of the extension + */ +function getBeginOfScriptCalls(stack) { + const extensionID = browser.runtime.getURL(""); + const lines = (typeof(stack) !== "string") ? stack : splitStack(stack); + for (let i=0; i= maxLogCount) { + return true; + } else if (!(key in logCounter)) { + logCounter[key] = 1; + } else { + logCounter[key] += 1; + } + return false; +} + +// Recursively generates a path for an element +function getPathToDomElement(element, visibilityAttr) { + if (element === document.body) { + return element.tagName; + } + if (element.parentNode === null) { + return "NULL/" + element.tagName; + } + + let siblingIndex = 1; + const siblings = element.parentNode.childNodes; + for (let i = 0; i < siblings.length; i++) { + const sibling = siblings[i]; + if (sibling === element) { + let path = getPathToDomElement(element.parentNode, visibilityAttr); + path += "/" + element.tagName + "[" + siblingIndex; + path += "," + element.id; + path += "," + element.className; + if (visibilityAttr) { + path += "," + element.hidden; + path += "," + element.style.display; + path += "," + element.style.visibility; + } + if (element.tagName === "A") { + path += "," + element.href; + } + path += "]"; + return path; + } + if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { + siblingIndex++; + } + } +} + +function getOriginatingScriptContext(getCallStack = false, isCall = false){ + const trace = getStackTrace().trim().split("\n"); + // return a context object even if there is an error + const empty_context = { + scriptUrl: "", + scriptLine: "", + scriptCol: "", + funcName: "", + scriptLocEval: "", + callStack: "", + }; + if (trace.length < 4) { + return empty_context; + } + + let traceStart = getBeginOfScriptCalls(trace); + if (traceStart == -1){ + // If not included, use heuristic, 0-3 or 0-2 are OpenWPMs functions + traceStart = isCall ? 3 : 4; + } + const callSite = trace[traceStart]; + if (!callSite) { + return empty_context; + } + /* + * Stack frame format is simply: FUNC_NAME@FILENAME:LINE_NO:COLUMN_NO + * + * If eval or Function is involved we have an additional part after the FILENAME, e.g.: + * FUNC_NAME@FILENAME line 123 > eval line 1 > eval:LINE_NO:COLUMN_NO + * or FUNC_NAME@FILENAME line 234 > Function:LINE_NO:COLUMN_NO + * + * We store the part between the FILENAME and the LINE_NO in scriptLocEval + */ + try { + let scriptUrl = ""; + let scriptLocEval = ""; // for eval or Function calls + const callSiteParts = callSite.split("@"); + const funcName = callSiteParts[0] || ""; + const items = rsplit(callSiteParts[1], ":", 2); + const columnNo = items[items.length - 1]; + const lineNo = items[items.length - 2]; + const scriptFileName = items[items.length - 3] || ""; + const lineNoIdx = scriptFileName.indexOf(" line "); // line in the URL means eval or Function + if (lineNoIdx === -1) { + scriptUrl = scriptFileName; // TODO: sometimes we have filename only, e.g. XX.js + } else { + scriptUrl = scriptFileName.slice(0, lineNoIdx); + scriptLocEval = scriptFileName.slice( + lineNoIdx + 1, + scriptFileName.length, + ); + } + const callContext = { + scriptUrl, + scriptLine: lineNo, + scriptCol: columnNo, + funcName, + scriptLocEval, + callStack: getCallStack ? trace.slice(3).join("\n").trim() : "", + }; + return callContext; + } catch (e) { + console.log( + "OpenWPM: Error parsing the script context", + e.toString(), + callSite, + ); + return empty_context; + } +} + +function logErrorToConsole(error, context = false) { + console.error("OpenWPM: Error name: " + error.name); + console.error("OpenWPM: Error message: " + error.message); + console.error("OpenWPM: Error filename: " + error.fileName); + console.error("OpenWPM: Error line number: " + error.lineNumber); + console.error("OpenWPM: Error stack: " + error.stack); + if (context) { + console.error("OpenWPM: Error context: " + JSON.stringify(context)); + } +} + +// For gets, sets, etc. on a single value +function logValue( + instrumentedVariableName,//: string, + value,//: any, + operation,//: string, // from JSOperation object please + callContext,//: any, + logSettings = false,//: LogSettings, +){ + if (inLog) { + return; + } + inLog = true; + + const overLimit = updateCounterAndCheckIfOver( + callContext.scriptUrl, + instrumentedVariableName, + ); + if (overLimit) { + inLog = false; + return; + } + + const msg = { + operation, + symbol: instrumentedVariableName, + value: serializeObject(value, logSettings.logFunctionsAsStrings), + scriptUrl: callContext.scriptUrl, + scriptLine: callContext.scriptLine, + scriptCol: callContext.scriptCol, + funcName: callContext.funcName, + scriptLocEval: callContext.scriptLocEval, + callStack: callContext.callStack, + ordinal: ordinal++, + }; + + try { + notify("logValue", msg); + } catch (error) { + console.log("OpenWPM: Unsuccessful value log!"); + logErrorToConsole(error); + } + + inLog = false; +} + +// For functions +function logCall(instrumentedFunctionName, args, callContext, logSettings) { + if (inLog) { + return; + } + inLog = true; + const overLimit = updateCounterAndCheckIfOver(callContext.scriptUrl, instrumentedFunctionName); + if (overLimit) { + inLog = false; + return; + } + try { + // Convert special arguments array to a standard array for JSONifying + const serialArgs = []; + for (const arg of args) { + serialArgs.push(serializeObject(arg, false));//TODO: Get back to logSettings.logFunctionsAsStrings)); + } + const msg = { + operation: JSOperation.call, + symbol: instrumentedFunctionName, + args: serialArgs, + value: "", + scriptUrl: callContext.scriptUrl, + scriptLine: callContext.scriptLine, + scriptCol: callContext.scriptCol, + funcName: callContext.funcName, + scriptLocEval: callContext.scriptLocEval, + callStack: callContext.callStack, + ordinal: ordinal++, + }; + notify("logCall", msg); + } + catch (error) { + console.log("OpenWPM: Unsuccessful call log: " + instrumentedFunctionName); + console.log(error); + logErrorToConsole(error); + } + inLog = false; +} + +/********************************************************************************* +* New functionality +**********************************************************************************/ + +/** + * Provides the properties per prototype object + */ +Object.getPrototypeByDepth = function (subject, depth) { + if (subject === undefined) { + throw new Error("Can't get property names for undefined"); + } + if (depth === undefined || typeof(depth) !== "number" ) { + throw new Error("Depth "+ depth +" is invalid"); + } + let proto = subject; + for(let i=1; i<=depth; i++) { + proto = Object.getPrototypeOf(proto); + } + if (proto === undefined){ + throw new Error("Prototype was undefined. Too deep iteration?"); + } + return proto; +} + +/** + * Traverses the prototype chain to collect properties. Returns an array containing + * an object with the depth, propertyNames and scanned subject + */ +Object.getPropertyNamesPerDepth = function (subject, maxDepth=0) { + if (subject === undefined) { + throw new Error("Can't get property names for undefined"); + } + let res = []; + let depth = 0; + let properties = Object.getOwnPropertyNames(subject); + res.push({"depth": depth, "propertyNames":properties, "object":subject}); + let proto = Object.getPrototypeOf(subject); + + while (proto !== null, depth < maxDepth) { + depth++; + properties = Object.getOwnPropertyNames(proto); + res.push({"depth": depth, "propertyNames":properties, "object":proto}); + proto = Object.getPrototypeOf(proto); + } + return res; +} + +/** + * Finds a property along the prototype chain + */ +Object.findPropertyInChain = function (subject, propertyName) { + if (subject === undefined || propertyName === undefined) { + throw new Error("Object and property name must be defined"); + } + let properties = []; + let depth = 0; + while (subject !== null) { + properties = Object.getOwnPropertyNames(subject); + if (properties.includes(propertyName)){ + return {"depth": depth, "propertyName":propertyName}; + } + depth++; + subject = Object.getPrototypeOf(subject); + } + throw Error("Property not found. Check whether configuration is correct!"); +} + + +/* + * Get all keys for properties that shall be overwritten + */ +function getPropertyKeysToOverwrite(item){ + let res = []; + item.logSettings.overwrittenProperties.forEach(obj =>{ + res.push(obj.key); + }) + return res; +} + +function getContextualPrototypeFromString(context, objectAsString) { + const obj = context[objectAsString]; + if (obj) { + return (obj.prototype) ? obj.prototype : Object.getPrototypeOf(obj); + } else { + return undefined; + } +} + + +/** + * Prepares a list of properties that need to be instrumented + * Here, this can be a previous created list (settings.js: propertiesToInstrument) + * or all properties of a given object (settings.js: propertiesToInstrument is empty) + */ +function getObjectProperties(context, item){ + let propertiesToInstrument = item.logSettings.propertiesToInstrument; + const proto = getContextualPrototypeFromString(context, item["object"]); + if (!proto) { + throw Error("Object " + item['object'] + "was undefined."); + } + + if (propertiesToInstrument === undefined || !propertiesToInstrument.length) { + propertiesToInstrument = Object.getPropertyNamesPerDepth(proto, item['depth']); + // filter excluded and overwritten properties + const excluded = getPropertyKeysToOverwrite(item).concat(item.logSettings.excludedProperties); + propertiesToInstrument = filterPropertiesPerDepth(propertiesToInstrument, excluded); + }else{ + // include the object to each item + propertiesToInstrument.forEach(propertyList => { + propertyList["object"] = Object.getPrototypeByDepth(proto, propertyList["depth"]); + }); + } + return propertiesToInstrument; +} + + +/* + * Enables communication with a background script + * Must be injected in a private scope to the + * page context! + * + * @param details: property access details + */ +function notify(type, content){ + content.timeStamp = new Date().toISOString(); + browser.runtime.sendMessage({ + namespace: "javascript-instrumentation", + type, + data: content + }); +} + +function filterPropertiesPerDepth(collection, excluded){ + for (let i=0; i !excluded.includes(p)); + } + return collection; +} + +/* + * Injects a function into the page context + * + * @param func: Function that shall be exported + * @param context: target DOM + * @param name: Name of the function (e.g., get width) + */ +function exportCustomFunction(func, context, name){ + const targetObject = context.wrappedJSObject.Object.create(null); + const exportedTry = exportFunction(func, targetObject, {allowCrossOriginArguments: true, defineAs: name}); + return exportedTry; +} + +/* + * TODO: Add description + */ + +function injectFunction( + instrumentedFunction, + descriptor, + functionType, + pageObject, + propertyName){ + const exportedFunction = exportCustomFunction(instrumentedFunction, window, propertyName); + changeProperty(descriptor, pageObject, propertyName, functionType, exportedFunction); +} + + +/* + * Add notifications when a property is requested + * TODO: Bring everything together at this point + * + * @param original: the original getter/setter function + * @param object: + * @param args: + */ +function instrumentGetObjectProperty(identifier, original, newValue, object, args){ + const originalValue = original.call(object, ...args); + const callContext = getOriginatingScriptContext(true); + const returnValue = (newValue !== undefined) ? newValue : originalValue; + logValue( + identifier, + returnValue, + JSOperation.get, + callContext, + //logSettings + ); + return returnValue; +} + /* + * Add notifications when a property is set + * + * @param original: the original getter/setter function + * @param object: + * @param args: + */ +function instrumentSetObjectProperty(identifier, original, newValue, object, args){ + const callContext = getOriginatingScriptContext(true); + logValue( + identifier, + newValue, + (!!original) ? JSOperation.set : JSOperation.set_failed, + callContext, + //logSettings + ); + if (!original){ + return newValue; + } + else{ + return original.call(object, newValue); + } +} + +/* +* Creates a getter function +* +* @param descriptor: the descriptor of the original function +* @param funcName: Name of property/function that shall be overwritten +* @param newValue: in Case the value shall be changed +*/ +function generateGetter(identifier, descriptor, propertyName, newValue = undefined){ + const original = descriptor.get; + return Object.getOwnPropertyDescriptor( + { + get [propertyName](){ + return instrumentGetObjectProperty(identifier, original, newValue, this, arguments); + } + }, + propertyName).get; +} + +/* +* Creates a setter function +* +* @param descriptor: the descriptor of the original function +* @param funcName: Name of property/function that shall be overwritten +* @param newValue: in Case the value shall be changed +*/ +function generateSetter(identifier, descriptor, propertyName, newValue){ + const original = descriptor.set; + return Object.getOwnPropertyDescriptor( + { + set [propertyName](newValue){ + return instrumentSetObjectProperty(identifier, original, newValue, this, arguments); + } + }, + propertyName).set; +} + +/* + * Overwrites the prototype to access a property + * @param + */ +function changeProperty( + descriptor, + pageObject, + name, + method, + changed){ + descriptor[method] = changed; + Object.defineProperty(pageObject, name, descriptor); +} + + +/* + * Retrieves an object in a context + * + * @param context: the window object that is currently instrumented + * @param object: the subobject needed + */ +function getPageObjectInContext(context, context_object){ + if (context === undefined || context_object === undefined){ + return ; + } + return context[context_object].prototype || context[context_object]; +} + +/* + * TODO: Add description + */ +const getPropertyType = (object, property) => typeof(object[property]); + + +/* + * Entry point to creates (g/s)etter functions, + * instrument them and inject them to the page + * context + */ +function instrumentGetterSetter( + descriptor, + identifier, + pageObject, + propertyName, + newValue = undefined) + { + let instrumentedFunction; + const getFuncType = "get"; + const setFuncType = "set"; + + if (descriptor.hasOwnProperty(getFuncType)){ + instrumentedFunction = generateGetter(identifier, descriptor, propertyName, newValue); + injectFunction(instrumentedFunction, descriptor, getFuncType, pageObject, propertyName); + } + if (descriptor.hasOwnProperty(setFuncType)){ + instrumentedFunction = generateSetter(identifier, descriptor, propertyName); + injectFunction(instrumentedFunction, descriptor, setFuncType, pageObject, propertyName); + } +} + +/* + * TODO: Add description + */ +function functionGenerator(context, identifier, original, funcName) { + function temp(){ + let result; + const callContext = getOriginatingScriptContext(true, true); + logCall(identifier, + arguments, + callContext + ); + try{ + result = (arguments.length > 0) ? original.call(this, ...arguments) : original.call(this); + }catch(err){ + let fakeError = generateErrorObject(err); + throw fakeError; + } + return result; + } + return temp +} + + +/* + * TODO: Add description + */ +function instrumentFunction(context, descriptor, identifier, pageObject, propertyName) { + const original = descriptor.value; + const tempFunction = functionGenerator(context, identifier, original, propertyName); + const exportedFunction = exportCustomFunction(tempFunction, context, original.name); + changeProperty(descriptor, pageObject, propertyName, "value", exportedFunction); +} + + +/* + * Helper class to perform all needed functionality + * + * @param context: the window object that is currently instrumented + * @param object: child object that shall be instumented + */ +function instrument(context, item, depth, propertyName, newValue = undefined){ + try{ + const identifier = item["instrumentedName"] + "." + propertyName; + const initialPageObject = getPageObjectInContext(context.wrappedJSObject, item["object"]); + const pageObject = Object.getPrototypeByDepth(initialPageObject, depth); + const descriptor = Object.getPropertyDescriptor(pageObject, propertyName); + if (descriptor === undefined){ + // Do not do undefined descriptor. We can safely skip them + return; + } + if (typeof(descriptor.value) === "function"){ + instrumentFunction(context, descriptor, identifier, pageObject, propertyName); + } else { + instrumentGetterSetter(descriptor, identifier, pageObject, propertyName, newValue); + } + } catch (error){ + console.log(error); + console.log(error.stack); + return; + } +} + + +/* + * Checks if an object was already wrapped + * Unwrapped objects should be wrapped immediately + */ +let wrappedObjects = []; +function needsWrapper(object) { + if (wrappedObjects.some(obj => object === obj)){ + //console.log("is already wrapped:" + object); + return false; + } + wrappedObjects.push(object); + //console.log("Will be wrapped:" + object); + return true; +} + +function startInstrument(context){ + jsInstrumentationSettings.forEach(item => { + + // retrieve Object properties alont the chain + let propertyCollection; + try { + propertyCollection = getObjectProperties(context, item); + } catch (err){ + console.log(err); + return; + } + // Instrument each Property per object/prototype + if (propertyCollection[0] !== ""){ + // console.log(item["instrumentedName"]); + // console.log(propertyCollection); + propertyCollection.forEach(({depth, propertyNames, object}) => { + if (needsWrapper(object)){ + propertyNames.forEach(propertyName => instrument(context, item, depth, propertyName)); + } + }); + } + // Instrument properties and overwrite their return value + if (item.logSettings.overwrittenProperties){ + item.logSettings.overwrittenProperties.forEach(({key: name, value}) => { + const proto = getContextualPrototypeFromString(context, item["object"]); + if (proto){ + let {depth, propertyName} = Object.findPropertyInChain(proto, name); + instrument(context, item, depth, propertyName, value); + } else { + console.log("Could not instrument " + item["object"] + ". Encountered undefined object."); + } + }); + } + }); + //console.log(wrappedObjects); +} + +export { + startInstrument, + exportCustomFunction +}; \ No newline at end of file diff --git a/Extension/src/stealth/settings.ts b/Extension/src/stealth/settings.ts new file mode 100644 index 000000000..06d7c8888 --- /dev/null +++ b/Extension/src/stealth/settings.ts @@ -0,0 +1,242 @@ +export const jsInstrumentationSettings = [ + {"object":"ScriptProcessorNode",// Depcrecated. Replaced by AudioWorkletNode + "instrumentedName":"ScriptProcessorNode", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"AudioWorkletNode", + "instrumentedName":"AudioWorkletNode", + "depth":1, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"GainNode", + "instrumentedName":"GainNode", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"AnalyserNode", + "instrumentedName":"AnalyserNode", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"OscillatorNode", + "instrumentedName":"OscillatorNode", + "depth":1, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + // Add shared prototype by AnalyserNode, OscillatorNode, ScriptProcessorNode, GainNode, ScriptProcessorNode + {"object":"AnalyserNode", + "instrumentedName":"Node", + "depth":1, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"OfflineAudioContext", + "instrumentedName":"OfflineAudioContext", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"AudioContext", + "instrumentedName":"AudioContext", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + +// Add shared prototype by AudioContenxt/OfflineAudioContext + {"object":"AudioContext", + "instrumentedName":"[AudioContenxt|OfflineAudioContext]", + "depth":1, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"RTCPeerConnection", + "instrumentedName":"RTCPeerConnection", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"HTMLCanvasElement", + "instrumentedName":"HTMLCanvasElement", + "depth":1, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":["style", "offsetWidth", "offsetHeight"], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"Storage", + "instrumentedName":"Storage", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"Navigator", + "instrumentedName":"Navigator", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[{"key":"webdriver", "value":false, "level":0}], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"CanvasRenderingContext2D", + "instrumentedName":"CanvasRenderingContext2D", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[ + "transform", + "globalAlpha", + "clearRect", + "closePath", + "canvas", + "quadraticCurveTo", + "lineTo", + "moveTo", + "setTransform", + "drawImage", + "beginPath", + "translate"], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"Screen", + "instrumentedName":"Screen", + "depth":0, + "logSettings":{"propertiesToInstrument":[], + // in OpenWPM is only this one used: + //{"depth":0, "propertyNames":["colorDepth","pixelDepth" + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":false, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + + {"object":"document", + "instrumentedName":"document", + "depth":0, + "logSettings":{"propertiesToInstrument":[{"depth":2, "propertyNames":["referrer"]}], + "nonExistingPropertiesToInstrument":[], + "excludedProperties":[], + "overwrittenProperties":[], + "logCallStack":true, + "logFunctionsAsStrings":false, + "logFunctionGets":false, + "preventSets":false, + "recursive":false, + "depth":5}}, + +]; \ No newline at end of file diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts new file mode 100644 index 000000000..95f101eb3 --- /dev/null +++ b/Extension/src/stealth/stealth.ts @@ -0,0 +1,381 @@ +"use strict"; +/* Taken from https://github.com/kkapsner/CanvasBlocker with small changes + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { startInstrument as instrument, + exportCustomFunction } from "./instrument"; + +// Declaring some local trackers +const interceptedWindows = new WeakMap(); +const proxies = new Map(); +const changedToStrings = new WeakMap(); + +// Entry point for this extension +(function(){ + // console.log("Starting frame script"); + try{ + interceptWindow(window); + } catch (error) { + console.log("Instrumentation initialisation crashed. Reason: " + error); + console.log(error.stack); + } + // console.log("Starting frame script"); +})(); + + +function interceptWindow(context){ + let wrappedTry; + try { + const href = context.location.href; + wrappedTry = getWrapped(context); + } catch (error){ + // we are unable to read the location due to SOP + // therefore we also can not intercept anything. + //console.log("NOT intercepting window due to SOP: ", context); + return false; + } + const wrappedWindow = wrappedTry; + + if (interceptedWindows.get(wrappedWindow)){ + //console.log("Already intercepted: ", context); + return false; + } + // console.log("intercepting window", context); + instrument(context); + interceptedWindows.set(wrappedWindow, true); + + //console.log("prepare to intercept "+ context.length +" (i)frames."); + function interceptAllFrames(){ + const currentLength = context.length; + for (let i = currentLength; i--;){ + if (!interceptedWindows.get(wrappedWindow[i])){ + interceptWindow(context[i]); + } + } + } + protectAllFrames(context, wrappedWindow, interceptWindow, interceptAllFrames); + return true; +} + +function protectAllFrames(context, wrappedWindow, singleCallback, allCallback){ + const changeWindowProperty = createChangeProperty(context); + if (!changeWindowProperty){ + return; + } + + const api = {context, wrappedWindow, changeWindowProperty, singleCallback, allCallback}; + + protectFrameProperties(api); + + protectDOMModifications(api); + + // MutationObserver to intercept iFrames while generating the DOM. + api.observe = enableMutationObserver(api); + + // MutationObserver does not trigger fast enough when document.write is used + protectDocumentWrite(api); + + protectWindowOpen(api); + }; + +function getWrapped(context) { + return context && (context.wrappedJSObject || context); +} + + +function createChangeProperty(window){ + const changeWindowProperty = function (object, name, type, changed){ + const descriptor = Object.getOwnPropertyDescriptor(object, name); + const original = descriptor[type]; + if ((typeof changed) === "function"){ + changed = createProxyFunction(window, original, changed); + } + changePropertyFunc(window, {object, name, type, changed}); + } + return changeWindowProperty; +} + +function createProxyFunction(context, original, replacement){ + if (!changedToStrings.get(context)){ + changedToStrings.set(context, true); + const functionPrototype = getWrapped(context).Function.prototype; + const toString = functionPrototype.toString; + changePropertyFunc( + context, + { + object: functionPrototype, + name: "toString", + type: "value", + changed: createProxyFunction( + context, + toString, + function(){ + return proxies.get(this) || toString.call(this); + } + ) + }); + } + const handler = getWrapped(context).Object.create(null); + handler.apply = exportCustomFunction(function(target, thisArgs, args){ + try { + return args.length? + replacement.call(thisArgs, ...args): + replacement.call(thisArgs); + } + catch (error){ + try { + return original.apply(thisArgs, args); + } + catch (error){ + return target.apply(thisArgs, args); + } + } + }, context, ""); + const proxy = new context.Proxy(original, handler); + proxies.set(proxy, original.toString()); + return getWrapped(proxy); +}; + +function changePropertyFunc(context, {object, name, type, changed}){ + // Removed tracker for changed properties + const descriptor = Object.getOwnPropertyDescriptor(object, name); + const original = descriptor[type]; + descriptor[type] = changed; + Object.defineProperty(object, name, descriptor); +}; + + +function protectFrameProperties({context, wrappedWindow, changeWindowProperty, singleCallback}){ + ["HTMLIFrameElement", "HTMLFrameElement"].forEach(function(constructorName){ + const constructor = context[constructorName]; + const wrappedConstructor = wrappedWindow[constructorName]; + + const contentWindowDescriptor = Object.getOwnPropertyDescriptor( + constructor.prototype, + "contentWindow" + ); + //TODO: Continue here!!!! + const originalContentWindowGetter = contentWindowDescriptor.get; + const contentWindowTemp = { + get contentWindow(){ + const window = originalContentWindowGetter.call(this); + if (window){ + // TODO: What is singleCallback - Instrumenting everything? + singleCallback(window); + } + return window; + } + }; + changeWindowProperty(wrappedConstructor.prototype, "contentWindow", "get", + Object.getOwnPropertyDescriptor(contentWindowTemp, "contentWindow").get + ); + + const contentDocumentDescriptor = Object.getOwnPropertyDescriptor( + constructor.prototype, + "contentDocument" + ); + const originalContentDocumentGetter = contentDocumentDescriptor.get; + const contentDocumentTemp = { + get contentDocument(){ + const document = originalContentDocumentGetter.call(this); + if (document){ + singleCallback(document.defaultView); + } + return document; + } + }; + changeWindowProperty(wrappedConstructor.prototype, "contentDocument", "get", + Object.getOwnPropertyDescriptor(contentDocumentTemp, "contentDocument").get + ); + }); +} + +function protectDOMModifications({context, wrappedWindow, changeWindowProperty, allCallback}){ + [ + // useless as length could be obtained before the iframe is created and window.frames === window + // { + // object: wrappedWindow, + // methods: [], + // getters: ["length", "frames"], + // setters: [] + // }, + { + object: wrappedWindow.Node.prototype, + methods: ["appendChild", "insertBefore", "replaceChild"], + getters: [], + setters: [] + }, + { + object: wrappedWindow.Element.prototype, + methods: [ + "append", "prepend", + "insertAdjacentElement", "insertAdjacentHTML", "insertAdjacentText", + "replaceWith" + ], + getters: [], + setters: [ + "innerHTML", + "outerHTML" + ] + } + ].forEach(function(protectionDefinition){ + const object = protectionDefinition.object; + protectionDefinition.methods.forEach(function(method){ + const descriptor = Object.getOwnPropertyDescriptor(object, method); + const original = descriptor.value; + changeWindowProperty(object, method, "value", class { + [method](){ + const value = arguments.length? + original.call(this, ...arguments): + original.call(this); + allCallback(); + return value; + } + }.prototype[method]); + }); + protectionDefinition.getters.forEach(function(property){ + const temp = { + get [property](){ + const ret = this[property]; + allCallback(); + return ret; + } + }; + changeWindowProperty(object, property, "get", + Object.getOwnPropertyDescriptor(temp, property).get + ); + }); + protectionDefinition.setters.forEach(function(property){ + const descriptor = Object.getOwnPropertyDescriptor(object, property); + const setter = descriptor.set; + const temp = { + set [property](value){ + const ret = setter.call(this, value); + allCallback(); + return ret; + } + }; + changeWindowProperty(object, property, "set", + Object.getOwnPropertyDescriptor(temp, property).set + ); + }); + }); +} + +function enableMutationObserver({context, allCallback}){ + const observer = new MutationObserver(allCallback); + let observing = false; + function observe(){ + if ( + !observing && + context.document + ){ + observer.observe(context.document, {subtree: true, childList: true}); + observing = true; + } + } + observe(); + context.document.addEventListener("DOMContentLoaded", function(){ + if (observing){ + observer.disconnect(); + observing = false; + } + }); + return observe; +} + +function protectDocumentWrite({context, wrappedWindow, changeWindowProperty, observe, allCallback}){ + const documentWriteDescriptorOnHTMLDocument = Object.getOwnPropertyDescriptor( + wrappedWindow.HTMLDocument.prototype, + "write" + ); + const documentWriteDescriptor = documentWriteDescriptorOnHTMLDocument || Object.getOwnPropertyDescriptor( + wrappedWindow.Document.prototype, + "write" + ); + const documentWrite = documentWriteDescriptor.value; + changeWindowProperty( + documentWriteDescriptorOnHTMLDocument? + wrappedWindow.HTMLDocument.prototype: + wrappedWindow.Document.prototype, + "write", "value", function write(markup){ + for (let i = 0, l = arguments.length; i < l; i += 1){ + const str = "" + arguments[i]; + // weird problem with waterfox and google docs + const parts = ( + str.match(/^\s*. + + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/crawler.py b/crawler.py index b1815eec6..73f5014cf 100644 --- a/crawler.py +++ b/crawler.py @@ -48,6 +48,7 @@ JS_INSTRUMENT_SETTINGS = json.loads( os.getenv("JS_INSTRUMENT_SETTINGS", '["collection_fingerprinting"]') ) +STEALTH_JS_INSTRUMENT = os.getenv("STEALTH_JS_INSTRUMENT", "1") == "1" SAVE_CONTENT = os.getenv("SAVE_CONTENT", "") PREFS = os.getenv("PREFS", None) @@ -79,6 +80,7 @@ browser_params[i].callstack_instrument = CALLSTACK_INSTRUMENT browser_params[i].js_instrument = JS_INSTRUMENT browser_params[i].js_instrument_settings = JS_INSTRUMENT_SETTINGS + browser_params[i].stealth_js_instrument = STEALTH_JS_INSTRUMENT if SAVE_CONTENT == "1": browser_params[i].save_content = True elif SAVE_CONTENT == "0": @@ -127,6 +129,7 @@ scope.set_tag("NAVIGATION_INSTRUMENT", NAVIGATION_INSTRUMENT) scope.set_tag("JS_INSTRUMENT", JS_INSTRUMENT) scope.set_tag("JS_INSTRUMENT_SETTINGS", JS_INSTRUMENT_SETTINGS) + scope.set_tag("STEALTH_JS_INSTRUMENT", STEALTH_JS_INSTRUMENT) scope.set_tag("CALLSTACK_INSTRUMENT", CALLSTACK_INSTRUMENT) scope.set_tag("SAVE_CONTENT", SAVE_CONTENT) scope.set_tag("DWELL_TIME", DWELL_TIME) diff --git a/docs/Configuration.md b/docs/Configuration.md index d19aa5a7b..afc9bea31 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -211,6 +211,13 @@ To activate a given instrument set `browser_params[i].instrument_name = True` In instances, such as `fetch`, where you do not need to specify `window.fetch`, but can use the alias `fetch`, in JavaScript code. The instrumentation `{"window": ["fetch",]}` will pick up calls to both `fetch()` and `window.fetch()`. +### `stealth_js_instrument` +- Improves the JS instrument described above by + - securing the communication between page and background scripts + - mitigating iframe escapes + - hiding/removing detectable properties + + ### `navigation_instrument` TODO @@ -281,7 +288,7 @@ configuration dictionary. This should be a `Path` object pointing to the or by manually tarring a firefox profile directory. > Please note that you must tar the contents of the profile directory -> and not the directory itself. +> and not the directory itself. > (For an example of the difference please see > [here](https://github.com/openwpm/OpenWPM/issues/790#issuecomment-791316632)) @@ -382,5 +389,5 @@ Response body content to save only specific types of files, for instance `browser_params.save_content = "image,script"` to save Images and Javascript files. This will lessen the performance impact of this instrumentation - when a large number of browsers are used in parallel. + when a large number of browsers are used in parallel. - You will also need to import LevelDbProvider from openwpm/storage/leveldb.py and instantiate it in the TaskManager in demo.py diff --git a/openwpm/config.py b/openwpm/config.py index 1898d3901..e3bbd9507 100644 --- a/openwpm/config.py +++ b/openwpm/config.py @@ -84,6 +84,7 @@ class BrowserParams(DataClassJsonMixin): default_factory=lambda: ["collection_fingerprinting"] ) http_instrument: bool = False + stealth_js_instrument: bool = False navigation_instrument: bool = False save_content: Union[bool, str] = False callstack_instrument: bool = False From fa8d5219c5fc3342bc14517d7d10d2c9bdbb1ff6 Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Tue, 17 May 2022 08:43:35 +0200 Subject: [PATCH 02/16] Reset indentation --- Extension/src/stealth/stealth.ts | 76 ++++++++++++++++---------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts index 95f101eb3..31221be91 100644 --- a/Extension/src/stealth/stealth.ts +++ b/Extension/src/stealth/stealth.ts @@ -105,17 +105,18 @@ function createProxyFunction(context, original, replacement){ changePropertyFunc( context, { - object: functionPrototype, - name: "toString", - type: "value", - changed: createProxyFunction( - context, - toString, - function(){ - return proxies.get(this) || toString.call(this); - } - ) - }); + object: functionPrototype, + name: "toString", + type: "value", + changed: createProxyFunction( + context, + toString, + function(){ + return proxies.get(this) || toString.call(this); + } + ) + } + ); } const handler = getWrapped(context).Object.create(null); handler.apply = exportCustomFunction(function(target, thisArgs, args){ @@ -149,40 +150,39 @@ function changePropertyFunc(context, {object, name, type, changed}){ function protectFrameProperties({context, wrappedWindow, changeWindowProperty, singleCallback}){ ["HTMLIFrameElement", "HTMLFrameElement"].forEach(function(constructorName){ - const constructor = context[constructorName]; - const wrappedConstructor = wrappedWindow[constructorName]; + const constructor = context[constructorName]; + const wrappedConstructor = wrappedWindow[constructorName]; - const contentWindowDescriptor = Object.getOwnPropertyDescriptor( - constructor.prototype, - "contentWindow" - ); + const contentWindowDescriptor = Object.getOwnPropertyDescriptor( + constructor.prototype, + "contentWindow" + ); //TODO: Continue here!!!! - const originalContentWindowGetter = contentWindowDescriptor.get; - const contentWindowTemp = { - get contentWindow(){ - const window = originalContentWindowGetter.call(this); - if (window){ - // TODO: What is singleCallback - Instrumenting everything? - singleCallback(window); - } - return window; - } - }; - changeWindowProperty(wrappedConstructor.prototype, "contentWindow", "get", - Object.getOwnPropertyDescriptor(contentWindowTemp, "contentWindow").get - ); + const originalContentWindowGetter = contentWindowDescriptor.get; + const contentWindowTemp = { + get contentWindow(){ + const window = originalContentWindowGetter.call(this); + if (window){ + singleCallback(window); + } + return window; + } + }; + changeWindowProperty(wrappedConstructor.prototype, "contentWindow", "get", + Object.getOwnPropertyDescriptor(contentWindowTemp, "contentWindow").get + ); - const contentDocumentDescriptor = Object.getOwnPropertyDescriptor( - constructor.prototype, - "contentDocument" + const contentDocumentDescriptor = Object.getOwnPropertyDescriptor( + constructor.prototype, + "contentDocument" ); const originalContentDocumentGetter = contentDocumentDescriptor.get; const contentDocumentTemp = { get contentDocument(){ - const document = originalContentDocumentGetter.call(this); - if (document){ - singleCallback(document.defaultView); - } + const document = originalContentDocumentGetter.call(this); + if (document){ + singleCallback(document.defaultView); + } return document; } }; From a1e10d3c870609a5e85299059e78411c9f79b600 Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Tue, 17 May 2022 08:46:39 +0200 Subject: [PATCH 03/16] Reset indentation --- Extension/src/stealth/stealth.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts index 31221be91..602ad0655 100644 --- a/Extension/src/stealth/stealth.ts +++ b/Extension/src/stealth/stealth.ts @@ -175,20 +175,20 @@ function protectFrameProperties({context, wrappedWindow, changeWindowProperty, s const contentDocumentDescriptor = Object.getOwnPropertyDescriptor( constructor.prototype, "contentDocument" - ); - const originalContentDocumentGetter = contentDocumentDescriptor.get; - const contentDocumentTemp = { - get contentDocument(){ + ); + const originalContentDocumentGetter = contentDocumentDescriptor.get; + const contentDocumentTemp = { + get contentDocument(){ const document = originalContentDocumentGetter.call(this); if (document){ singleCallback(document.defaultView); } - return document; - } - }; - changeWindowProperty(wrappedConstructor.prototype, "contentDocument", "get", - Object.getOwnPropertyDescriptor(contentDocumentTemp, "contentDocument").get - ); + return document; + } + }; + changeWindowProperty(wrappedConstructor.prototype, "contentDocument", "get", + Object.getOwnPropertyDescriptor(contentDocumentTemp, "contentDocument").get + ); }); } From 36da764b29b3cc2b7114c4911279ca6df8999504 Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Tue, 17 May 2022 08:53:38 +0200 Subject: [PATCH 04/16] Reset indentation --- Extension/src/stealth/instrument.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extension/src/stealth/instrument.ts b/Extension/src/stealth/instrument.ts index 55cfb90e0..9fe260380 100644 --- a/Extension/src/stealth/instrument.ts +++ b/Extension/src/stealth/instrument.ts @@ -571,7 +571,7 @@ function changeProperty( name, method, changed){ - descriptor[method] = changed; + descriptor[method] = changed; Object.defineProperty(pageObject, name, descriptor); } From 508cd45c06f1501be6cc94196bf2e14a6918a9e4 Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Sat, 11 Jun 2022 17:03:04 +0200 Subject: [PATCH 05/16] Clean up code --- Extension/src/stealth/instrument.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/Extension/src/stealth/instrument.ts b/Extension/src/stealth/instrument.ts index 9fe260380..16c950637 100644 --- a/Extension/src/stealth/instrument.ts +++ b/Extension/src/stealth/instrument.ts @@ -267,7 +267,8 @@ function logValue( notify("logValue", msg); } catch (error) { console.log("OpenWPM: Unsuccessful value log!"); - logErrorToConsole(error); + // Activate for debugging purpose + //logErrorToConsole(error); } inLog = false; @@ -307,8 +308,9 @@ function logCall(instrumentedFunctionName, args, callContext, logSettings) { } catch (error) { console.log("OpenWPM: Unsuccessful call log: " + instrumentedFunctionName); - console.log(error); - logErrorToConsole(error); + // Activate for debugging purpose + //console.log(error); + //logErrorToConsole(error); } inLog = false; } @@ -411,7 +413,7 @@ function getObjectProperties(context, item){ let propertiesToInstrument = item.logSettings.propertiesToInstrument; const proto = getContextualPrototypeFromString(context, item["object"]); if (!proto) { - throw Error("Object " + item['object'] + "was undefined."); + throw Error("Object " + item['object'] + " was undefined."); } if (propertiesToInstrument === undefined || !propertiesToInstrument.length) { @@ -677,8 +679,8 @@ function instrument(context, item, depth, propertyName, newValue = undefined){ instrumentGetterSetter(descriptor, identifier, pageObject, propertyName, newValue); } } catch (error){ - console.log(error); - console.log(error.stack); + console.error(error); + console.error(error.stack); return; } } @@ -691,29 +693,25 @@ function instrument(context, item, depth, propertyName, newValue = undefined){ let wrappedObjects = []; function needsWrapper(object) { if (wrappedObjects.some(obj => object === obj)){ - //console.log("is already wrapped:" + object); return false; } wrappedObjects.push(object); - //console.log("Will be wrapped:" + object); return true; } function startInstrument(context){ - jsInstrumentationSettings.forEach(item => { + for (const item of jsInstrumentationSettings) { // retrieve Object properties alont the chain let propertyCollection; try { propertyCollection = getObjectProperties(context, item); } catch (err){ - console.log(err); - return; + console.error(err); + continue; } // Instrument each Property per object/prototype if (propertyCollection[0] !== ""){ - // console.log(item["instrumentedName"]); - // console.log(propertyCollection); propertyCollection.forEach(({depth, propertyNames, object}) => { if (needsWrapper(object)){ propertyNames.forEach(propertyName => instrument(context, item, depth, propertyName)); @@ -728,12 +726,11 @@ function startInstrument(context){ let {depth, propertyName} = Object.findPropertyInChain(proto, name); instrument(context, item, depth, propertyName, value); } else { - console.log("Could not instrument " + item["object"] + ". Encountered undefined object."); + console.error("Could not instrument " + item["object"] + ". Encountered undefined object."); } }); } - }); - //console.log(wrappedObjects); + } } export { From 086c4cf620649ec4c46b779d6dc223d260380eed Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Fri, 28 Oct 2022 18:54:03 +0200 Subject: [PATCH 06/16] update env yaml to comply with 0.20.0 --- environment.yaml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/environment.yaml b/environment.yaml index f99603ccf..4b99abf8c 100644 --- a/environment.yaml +++ b/environment.yaml @@ -2,37 +2,37 @@ channels: - conda-forge - main dependencies: -- beautifulsoup4=4.12.2 +- beautifulsoup4=4.12.3 - black=23.12.1 - click=8.1.7 - codecov=2.1.13 - dill=0.3.7 - easyprocess=1.1 - gcsfs=2023.12.2.post1 -- geckodriver=0.33.0 -- ipython=8.19.0 +- geckodriver=0.34.0 +- ipython=8.20.0 - isort=5.13.2 - leveldb=1.23 - multiprocess=0.70.15 - mypy=1.8.0 - nodejs=20.9.0 -- pandas=2.1.4 -- pillow=10.1.0 +- pandas=2.2.0 +- pillow=10.2.0 - pip=23.3.2 -- plyvel=1.5.0 +- plyvel=1.5.1 - pre-commit=3.6.0 -- psutil=5.9.7 +- psutil=5.9.8 - pyarrow=14.0.2 -- pytest-asyncio=0.23.2 +- pytest-asyncio=0.23.3 - pytest-cov=4.1.0 -- pytest=7.4.3 +- pytest=7.4.4 - python=3.12.1 - pyvirtualdisplay=2.2 - recommonmark=0.7.1 - redis-py=5.0.1 - s3fs=2023.12.2 -- selenium=4.16.0 -- sentry-sdk=1.39.1 +- selenium=4.17.2 +- sentry-sdk=1.39.2 - sphinx-markdown-tables=0.0.17 - sphinx=7.2.6 - tabulate=0.9.0 @@ -41,9 +41,9 @@ dependencies: - pip: - dataclasses-json==0.6.3 - domain-utils==0.7.1 - - jsonschema==4.20.0 + - jsonschema==4.21.1 - tranco==0.7.1 - types-pyyaml==6.0.12.12 - - types-redis==4.6.0.11 - - types-tabulate==0.9.0.3 + - types-redis==4.6.0.20240106 + - types-tabulate==0.9.0.20240106 name: openwpm From f087253b4609b186a8fb409345bdfb6892587ef0 Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Tue, 1 Nov 2022 10:22:18 +0100 Subject: [PATCH 07/16] Add OpenWPM_hide instructions --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e85f558be..42b68d28f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,117 @@ +OpenWPMhide +====================== +Full details are described in our paper, which can be found +on our [website](https://bkrumnow.github.io/openwpm-reliability/). + +Deployment +---------- +1. Follow the instructions from the original description +to install OpenWPM ([see below](#installation)). +2. Adjuste the [settings.js](https://github.com/bkrumnow/OpenWPM/blob/stealth_extension/Extension/firefox/stealth.js/settings.js) +to determine which properties shall be instrumented. +3. Recompile the extension if you do any changes to make them effective +4. Add the new features to your OpenWPM clients, as described in the next section +5. Run your OpenWPM client as + +To recompile the extension run the following commands: + +```sh +cd Extension/firefox && npm install && run build && cp dist/*.zip ./openwpm.xpi && cd ../.. +``` + +Main features +------------ +1. Adjusting the window position +2. Adjusting screen resolution +3. Hardened JavaScript instrument +4. Overwriting the _webdriver_ attribute + + +### Window position & screen resolution +OpenWPMhide introduces two new parameters to the BrowserParams object. +Using both parameters works as follows: + +```javascript +NUM_BROWSERS = 2 +browser_params = [BrowserParams(display_mode="native") for _ in range(NUM_BROWSERS)] +for browser_param in browser_params: + browser_param.resolution = (1520, 630) + browser_param.position = (50, 100) +``` + +### Hardened JavaScript instrument +Set _stealth_js_instrument_ to _True_ to activate the hardened version (similar as above): + +```javascript + NUM_BROWSERS = 2 + browser_params = [BrowserParams(display_mode="native") for _ in range(NUM_BROWSERS)] + for browser_param in browser_params: + browser_param.stealth_js_instrument = True +``` + +Use the [settings.js](https://github.com/bkrumnow/OpenWPM/blob/stealth_extension/Extension/firefox/stealth.js/settings.js) file to +define which properties will be recorded. While there is a number of properties already listed in the sample settings.js, you may +want to add others. Adding new properties requires to determine a property's position in the property chain. The position can be +retrieved in a Firefox browser via JavaScript reflect. + +In your browser, add this function via the console: + +```javascript +Object.getPropertyNamesPerDepth = function (subject, maxDepth=10) { + if (subject === undefined) { + throw new Error("Can't get property names for undefined"); + } + let res = []; + let depth = 0; + let properties = Object.getOwnPropertyNames(subject); + res.push({"depth": depth, "propertyNames":properties, "object":subject}); + let proto = Object.getPrototypeOf(subject); + + while (proto !== null, depth < maxDepth) { + depth++; + properties = Object.getOwnPropertyNames(proto); + res.push({"depth": depth, "propertyNames":properties, "object":proto}); + proto = Object.getPrototypeOf(proto); + if (proto==null){ + return res; + } + } + return res; +} +``` +Then check for property levels (this example checks the navigator object): +```javascript +Object.getPropertyNamesPerDepth(Object.getPrototypeOf(navigator)) +```` + +### Overwriting webdriver attribute +Overwriting is done by default when activating stealth_js_instrument. However, you may not want to use the JS recording, but +overwrite the property anyway. In this case, remove all entries settings.js, except the [default entry](https://github.com/bkrumnow/OpenWPM/blob/stealth_extension/Extension/firefox/stealth.js/settings.js#L172-L184) for the Navigator object. + + +Cite +----- +You can refer to our work as follows: + +How gullible are web measurement tools? A case study analysing and strengthening OpenWPM’s reliability. Benjamin Krumnow, Hugo Jonker, and Stefan Karsch. In Proc. 18th International Conference on emerging Networking EXperiments and Technologies (CoNEXT’22). ACM, 16 pages, doi: 10.1145/3555050.3569131, 2022. + +or + +```bibtex +@inproceedings{KJK22, + author = {Krumnow, Benjamin and Jonker, Hugo and + Karsch, Stefan}, + title = {How gullible are web measurement tools? {A} case study + analysing and strengthening {OpenWPM}’s reliability}, + booktitle = {Proc.\ 18th International Conference on emerging Networking + EXperiments and Technologies {(CoNEXT ’22)}}, + publisher = { {ACM} }, + year = {2022}, + address = {New York, NY, USA}, + pages = {16}, + doi = {10.1145/3555050.3569131} +} +``` # OpenWPM [![Documentation Status](https://readthedocs.org/projects/openwpm/badge/?version=latest)](https://openwpm.readthedocs.io/en/latest/?badge=latest) [![Build Status](https://github.com/openwpm/OpenWPM/workflows/Tests%20and%20linting/badge.svg?branch=master)](https://github.com/openwpm/OpenWPM/actions?query=branch%3Amaster) [![OpenWPM Matrix Channel](https://img.shields.io/matrix/OpenWPM:mozilla.org?label=Join%20us%20on%20matrix&server_fqdn=mozilla.modular.im)](https://matrix.to/#/#OpenWPM:mozilla.org?via=mozilla.org) @@ -199,7 +313,7 @@ For each of the data classes we offer a variety of storage providers, and you ar to implement your own, should the provided backends not be enough for you. We have an outstanding issue to enable saving content generated by commands, such as -screenshots and page dumps to unstructured storage (see [#232](https://github.com/openwpm/OpenWPM/issues/232)). +screenshots and page dumps to unstructured storage (see [#232](https://github.com/openwpm/OpenWPM/issues/232)). For now, they get saved to `manager_params.data_directory`. ### Local Storage From 0ee12ba14839068835ec3c12bb32ecfdb1ec2c6f Mon Sep 17 00:00:00 2001 From: bkrumnow Date: Tue, 1 Nov 2022 19:06:59 +0100 Subject: [PATCH 08/16] Add screen size and window position --- README.md | 19 +++++----- hide_commands/__init__.py | 1 + hide_commands/commands.py | 73 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 hide_commands/__init__.py create mode 100644 hide_commands/commands.py diff --git a/README.md b/README.md index 42b68d28f..f287f7132 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,22 @@ OpenWPMhide introduces two new parameters to the BrowserParams object Using both parameters works as follows: ```javascript -NUM_BROWSERS = 2 -browser_params = [BrowserParams(display_mode="native") for _ in range(NUM_BROWSERS)] -for browser_param in browser_params: - browser_param.resolution = (1520, 630) - browser_param.position = (50, 100) +from hide_commands import (SetResolution, SetPosition) +... +command_sequence = CommandSequence(...) + +command_sequence.append_command(SetResolution(width=1600, height=800), timeout=10) +command_sequence.append_command(SetPosition(x=50, y=200), timeout=10) ``` ### Hardened JavaScript instrument Set _stealth_js_instrument_ to _True_ to activate the hardened version (similar as above): ```javascript - NUM_BROWSERS = 2 - browser_params = [BrowserParams(display_mode="native") for _ in range(NUM_BROWSERS)] - for browser_param in browser_params: - browser_param.stealth_js_instrument = True +NUM_BROWSERS = 2 +browser_params = [BrowserParams(display_mode="native") for _ in range(NUM_BROWSERS)] +for browser_param in browser_params: + browser_param.stealth_js_instrument = True ``` Use the [settings.js](https://github.com/bkrumnow/OpenWPM/blob/stealth_extension/Extension/firefox/stealth.js/settings.js) file to diff --git a/hide_commands/__init__.py b/hide_commands/__init__.py new file mode 100644 index 000000000..f36d7748f --- /dev/null +++ b/hide_commands/__init__.py @@ -0,0 +1 @@ +from .commands import (SetResolution, SetPosition) \ No newline at end of file diff --git a/hide_commands/commands.py b/hide_commands/commands.py new file mode 100644 index 000000000..f23090a2c --- /dev/null +++ b/hide_commands/commands.py @@ -0,0 +1,73 @@ +""" This file aims to demonstrate how to write custom commands in OpenWPM + +Steps to have a custom command run as part of a CommandSequence + +1. Create a class that derives from BaseCommand +2. Implement the execute method +3. Append it to the CommandSequence +4. Execute the CommandSequence + +""" +import logging + +from tkinter import ttk + +from openwpm.commands.types import BaseCommand +from openwpm.config import BrowserParams, ManagerParams +from openwpm.socket_interface import ClientSocket + +from selenium.webdriver import Firefox + + +def get_screen_resolution(driver): + return driver.execute_script("return [screen.width, screen.height];") + + +class SetResolution(BaseCommand): + """ Sets the browser window resolution """ + def __init__(self, width, height) -> None: + self.logger = logging.getLogger("openwpm") + self.width = width + self.height = height + + def __repr__(self) -> str: + return "SetResolution" + + def execute( + self, + driver: Firefox, + browser_params: BrowserParams, + manager_params: ManagerParams, + extension_socket: ClientSocket, + ): + + self.logger.info(f"Setting window resolution to {self.width} x {self.height} ") + driver.set_window_size(self.width, self.height) + + resolution = get_screen_resolution(driver) + if resolution[0] <= self.width or resolution[1] <= self.height: + self.logger.warn( + f"Browser window resolution ({self.width} x {self.height}) exceeds " + + f"screen resolution ({resolution[0]} x {resolution[1]})") + + + +class SetPosition(BaseCommand): + """ Sets the browser window position """ + def __init__(self, x, y) -> None: + self.logger = logging.getLogger("openwpm") + self.x = x + self.y = y + + def __repr__(self) -> str: + return "SetPosition" + + def execute( + self, + driver: Firefox, + browser_params: BrowserParams, + manager_params: ManagerParams, + extension_socket: ClientSocket, + ): + + driver.set_window_position(self.x, self.y, windowHandle='current') \ No newline at end of file From eee69d067fd0cede08b62c28cc8d8c8a307d7abf Mon Sep 17 00:00:00 2001 From: vringar Date: Mon, 26 Jun 2023 17:11:22 +0200 Subject: [PATCH 09/16] fix(pre-commit): address pre-commit issues --- hide_commands/__init__.py | 2 +- hide_commands/commands.py | 37 ++++++++++++++++++------------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/hide_commands/__init__.py b/hide_commands/__init__.py index f36d7748f..3f71258af 100644 --- a/hide_commands/__init__.py +++ b/hide_commands/__init__.py @@ -1 +1 @@ -from .commands import (SetResolution, SetPosition) \ No newline at end of file +from .commands import SetPosition, SetResolution diff --git a/hide_commands/commands.py b/hide_commands/commands.py index f23090a2c..4abfcd95b 100644 --- a/hide_commands/commands.py +++ b/hide_commands/commands.py @@ -9,23 +9,23 @@ """ import logging - from tkinter import ttk +from selenium.webdriver import Firefox + from openwpm.commands.types import BaseCommand from openwpm.config import BrowserParams, ManagerParams from openwpm.socket_interface import ClientSocket -from selenium.webdriver import Firefox - -def get_screen_resolution(driver): +def get_screen_resolution(driver: Firefox) -> list[int]: return driver.execute_script("return [screen.width, screen.height];") - + class SetResolution(BaseCommand): - """ Sets the browser window resolution """ - def __init__(self, width, height) -> None: + """Sets the browser window resolution""" + + def __init__(self, width: int, height: int) -> None: self.logger = logging.getLogger("openwpm") self.width = width self.height = height @@ -39,22 +39,22 @@ def execute( browser_params: BrowserParams, manager_params: ManagerParams, extension_socket: ClientSocket, - ): - + ) -> None: self.logger.info(f"Setting window resolution to {self.width} x {self.height} ") driver.set_window_size(self.width, self.height) - + resolution = get_screen_resolution(driver) if resolution[0] <= self.width or resolution[1] <= self.height: self.logger.warn( - f"Browser window resolution ({self.width} x {self.height}) exceeds " + - f"screen resolution ({resolution[0]} x {resolution[1]})") - - + f"Browser window resolution ({self.width} x {self.height}) exceeds " + + f"screen resolution ({resolution[0]} x {resolution[1]})" + ) + class SetPosition(BaseCommand): - """ Sets the browser window position """ - def __init__(self, x, y) -> None: + """Sets the browser window position""" + + def __init__(self, x: int, y: int) -> None: self.logger = logging.getLogger("openwpm") self.x = x self.y = y @@ -68,6 +68,5 @@ def execute( browser_params: BrowserParams, manager_params: ManagerParams, extension_socket: ClientSocket, - ): - - driver.set_window_position(self.x, self.y, windowHandle='current') \ No newline at end of file + ) -> None: + driver.set_window_position(self.x, self.y, windowHandle="current") From 886e4f9cb4f31386c31bbf56200fcde9ebb56b27 Mon Sep 17 00:00:00 2001 From: vringar Date: Tue, 18 Jul 2023 17:34:46 +0200 Subject: [PATCH 10/16] WIP --- .../src/background/javascript-instrument.ts | 2 +- Extension/src/feature.ts | 20 +- Extension/src/stealth/error.ts | 96 +-- Extension/src/stealth/instrument.ts | 581 +++++++++------- Extension/src/stealth/settings.ts | 514 +++++++------- Extension/src/stealth/stealth.ts | 633 +++++++++--------- 6 files changed, 1026 insertions(+), 820 deletions(-) diff --git a/Extension/src/background/javascript-instrument.ts b/Extension/src/background/javascript-instrument.ts index 1e450ac12..2ed4e88ea 100644 --- a/Extension/src/background/javascript-instrument.ts +++ b/Extension/src/background/javascript-instrument.ts @@ -140,7 +140,7 @@ export class JavascriptInstrument { matchAboutBlank: true, }); } - const entryScript = (this.legacy) ? "/content.js" : "/stealth.js"; + const entryScript = this.legacy ? "/content.js" : "/stealth.js"; return browser.contentScripts.register({ js: [{ file: entryScript }], matches: [""], diff --git a/Extension/src/feature.ts b/Extension/src/feature.ts index c13b11113..88ff4d3d5 100644 --- a/Extension/src/feature.ts +++ b/Extension/src/feature.ts @@ -21,7 +21,7 @@ async function main() { navigation_instrument: true, cookie_instrument: true, js_instrument: true, - stealth_js_instrument:false, + stealth_js_instrument: false, cleaned_js_instrument_settings: [ { object: `window.CanvasRenderingContext2D.prototype`, @@ -73,16 +73,20 @@ async function main() { const cookieInstrument = new CookieInstrument(loggingDB); cookieInstrument.run(config.browser_id); } - if (config['stealth_js_instrument']) { + if (config.stealth_js_instrument) { loggingDB.logDebug("Stealth JavaScript Instrumentation enabled"); - let stealthJSInstrument = new JavascriptInstrument(loggingDB, false); - stealthJSInstrument.run(config['browser_id']); + const stealthJSInstrument = new JavascriptInstrument(loggingDB, false); + stealthJSInstrument.run(config.browser_id); await stealthJSInstrument.registerContentScript(); - } if (config['js_instrument']) { + } + if (config.js_instrument) { loggingDB.logDebug("Javascript instrumentation enabled"); - let jsInstrument = new JavascriptInstrument(loggingDB, true); - jsInstrument.run(config['browser_id']); - await jsInstrument.registerContentScript(config['testing'], config['cleaned_js_instrument_settings']); + const jsInstrument = new JavascriptInstrument(loggingDB, true); + jsInstrument.run(config.browser_id); + await jsInstrument.registerContentScript( + config.testing, + config.cleaned_js_instrument_settings, + ); } if (config.http_instrument) { diff --git a/Extension/src/stealth/error.ts b/Extension/src/stealth/error.ts index d45aa1424..7f45d2c5a 100644 --- a/Extension/src/stealth/error.ts +++ b/Extension/src/stealth/error.ts @@ -1,21 +1,21 @@ /* * Functionality to generate error objects */ -function generateErrorObject(err, context){ +function generateErrorObject(err, context) { // TODO: Pass context - context = (context !== undefined) ? context : window; - const cleaned = cleanErrorStack(err.stack) + context = context !== undefined ? context : window; + const cleaned = cleanErrorStack(err.stack); const stack = splitStack(cleaned); const lineInfo = getLineInfo(stack); const fileName = getFileName(stack); - let fakeError; - try{ + let fakeError: { lineNumber: any; columnNumber: any }; + try { // fake type, message, filename, column and line - const propertyName = "stack"; + // const propertyName = "stack"; fakeError = new context.wrappedJSObject[err.name](err.message, fileName); fakeError.lineNumber = lineInfo.lineNumber; fakeError.columnNumber = lineInfo.columnNumber; - }catch(error){ + } catch (error) { console.log("ERROR creation failed. Error was:" + error); } return fakeError; @@ -26,10 +26,10 @@ function generateErrorObject(err, context){ */ function cleanErrorStack(stack) { const extensionID = browser.runtime.getURL(""); - const lines = (typeof(stack) !== "string") ? stack : splitStack(stack); - lines.forEach(line =>{ - if (line.includes(extensionID)){ - stack = stack.replace(line+"\n",""); + const lines = typeof stack !== "string" ? stack : splitStack(stack); + lines.forEach((line) => { + if (line.includes(extensionID)) { + stack = stack.replace(line + "\n", ""); } }); return stack; @@ -40,9 +40,9 @@ function cleanErrorStack(stack) { */ function getBeginOfScriptCalls(stack) { const extensionID = browser.runtime.getURL(""); - const lines = (typeof(stack) !== "string") ? stack : splitStack(stack); - for (let i=0; i= maxLogCount) { return true; @@ -112,7 +116,7 @@ function updateCounterAndCheckIfOver(scriptUrl, symbol){ } // Recursively generates a path for an element -function getPathToDomElement(element, visibilityAttr) { +function getPathToDomElement(element, visibilityAttr: boolean = false) { if (element === document.body) { return element.tagName; } @@ -122,8 +126,7 @@ function getPathToDomElement(element, visibilityAttr) { let siblingIndex = 1; const siblings = element.parentNode.childNodes; - for (let i = 0; i < siblings.length; i++) { - const sibling = siblings[i]; + for (const sibling of siblings) { if (sibling === element) { let path = getPathToDomElement(element.parentNode, visibilityAttr); path += "/" + element.tagName + "[" + siblingIndex; @@ -146,7 +149,7 @@ function getPathToDomElement(element, visibilityAttr) { } } -function getOriginatingScriptContext(getCallStack = false, isCall = false){ +function getOriginatingScriptContext(getCallStack = false, isCall = false) { const trace = getStackTrace().trim().split("\n"); // return a context object even if there is an error const empty_context = { @@ -162,7 +165,7 @@ function getOriginatingScriptContext(getCallStack = false, isCall = false){ } let traceStart = getBeginOfScriptCalls(trace); - if (traceStart == -1){ + if (traceStart == -1) { // If not included, use heuristic, 0-3 or 0-2 are OpenWPMs functions traceStart = isCall ? 3 : 4; } @@ -217,25 +220,25 @@ function getOriginatingScriptContext(getCallStack = false, isCall = false){ } } -function logErrorToConsole(error, context = false) { - console.error("OpenWPM: Error name: " + error.name); - console.error("OpenWPM: Error message: " + error.message); - console.error("OpenWPM: Error filename: " + error.fileName); - console.error("OpenWPM: Error line number: " + error.lineNumber); - console.error("OpenWPM: Error stack: " + error.stack); - if (context) { - console.error("OpenWPM: Error context: " + JSON.stringify(context)); - } -} +// function logErrorToConsole(error, context = false) { +// console.error("OpenWPM: Error name: " + error.name); +// console.error("OpenWPM: Error message: " + error.message); +// console.error("OpenWPM: Error filename: " + error.fileName); +// console.error("OpenWPM: Error line number: " + error.lineNumber); +// console.error("OpenWPM: Error stack: " + error.stack); +// if (context) { +// console.error("OpenWPM: Error context: " + JSON.stringify(context)); +// } +// } // For gets, sets, etc. on a single value function logValue( - instrumentedVariableName,//: string, - value,//: any, - operation,//: string, // from JSOperation object please - callContext,//: any, - logSettings = false,//: LogSettings, -){ + instrumentedVariableName, // : string, + value, // : any, + operation, // : string, // from JSOperation object please + callContext, // : any, + logSettings = false, // : LogSettings, +) { if (inLog) { return; } @@ -264,60 +267,62 @@ function logValue( }; try { - notify("logValue", msg); + notify("logValue", msg); } catch (error) { console.log("OpenWPM: Unsuccessful value log!"); // Activate for debugging purpose - //logErrorToConsole(error); + // logErrorToConsole(error); } inLog = false; } // For functions -function logCall(instrumentedFunctionName, args, callContext, logSettings) { - if (inLog) { - return; - } - inLog = true; - const overLimit = updateCounterAndCheckIfOver(callContext.scriptUrl, instrumentedFunctionName); - if (overLimit) { - inLog = false; - return; - } - try { - // Convert special arguments array to a standard array for JSONifying - const serialArgs = []; - for (const arg of args) { - serialArgs.push(serializeObject(arg, false));//TODO: Get back to logSettings.logFunctionsAsStrings)); - } - const msg = { - operation: JSOperation.call, - symbol: instrumentedFunctionName, - args: serialArgs, - value: "", - scriptUrl: callContext.scriptUrl, - scriptLine: callContext.scriptLine, - scriptCol: callContext.scriptCol, - funcName: callContext.funcName, - scriptLocEval: callContext.scriptLocEval, - callStack: callContext.callStack, - ordinal: ordinal++, - }; - notify("logCall", msg); - } - catch (error) { - console.log("OpenWPM: Unsuccessful call log: " + instrumentedFunctionName); - // Activate for debugging purpose - //console.log(error); - //logErrorToConsole(error); - } +function logCall(instrumentedFunctionName, args, callContext, _logSettings) { + if (inLog) { + return; + } + inLog = true; + const overLimit = updateCounterAndCheckIfOver( + callContext.scriptUrl, + instrumentedFunctionName, + ); + if (overLimit) { inLog = false; + return; + } + try { + // Convert special arguments array to a standard array for JSONifying + const serialArgs = []; + for (const arg of args) { + serialArgs.push(serializeObject(arg, false)); // TODO: Get back to logSettings.logFunctionsAsStrings)); + } + const msg = { + operation: JSOperation.call, + symbol: instrumentedFunctionName, + args: serialArgs, + value: "", + scriptUrl: callContext.scriptUrl, + scriptLine: callContext.scriptLine, + scriptCol: callContext.scriptCol, + funcName: callContext.funcName, + scriptLocEval: callContext.scriptLocEval, + callStack: callContext.callStack, + ordinal: ordinal++, + }; + notify("logCall", msg); + } catch (error) { + console.log("OpenWPM: Unsuccessful call log: " + instrumentedFunctionName); + // Activate for debugging purpose + // console.log(error); + // logErrorToConsole(error); + } + inLog = false; } -/********************************************************************************* -* New functionality -**********************************************************************************/ +/** ******************************************************************************* + * New functionality + **********************************************************************************/ /** * Provides the properties per prototype object @@ -326,41 +331,41 @@ Object.getPrototypeByDepth = function (subject, depth) { if (subject === undefined) { throw new Error("Can't get property names for undefined"); } - if (depth === undefined || typeof(depth) !== "number" ) { - throw new Error("Depth "+ depth +" is invalid"); + if (depth === undefined || typeof depth !== "number") { + throw new Error("Depth " + depth + " is invalid"); } let proto = subject; - for(let i=1; i<=depth; i++) { + for (let i = 1; i <= depth; i++) { proto = Object.getPrototypeOf(proto); } - if (proto === undefined){ + if (proto === undefined) { throw new Error("Prototype was undefined. Too deep iteration?"); } return proto; -} +}; /** * Traverses the prototype chain to collect properties. Returns an array containing * an object with the depth, propertyNames and scanned subject */ -Object.getPropertyNamesPerDepth = function (subject, maxDepth=0) { +Object.getPropertyNamesPerDepth = function (subject, maxDepth = 0) { if (subject === undefined) { throw new Error("Can't get property names for undefined"); } - let res = []; + const res = []; let depth = 0; let properties = Object.getOwnPropertyNames(subject); - res.push({"depth": depth, "propertyNames":properties, "object":subject}); + res.push({ depth, propertyNames: properties, object: subject }); let proto = Object.getPrototypeOf(subject); - while (proto !== null, depth < maxDepth) { + while (proto !== null && depth < maxDepth) { depth++; properties = Object.getOwnPropertyNames(proto); - res.push({"depth": depth, "propertyNames":properties, "object":proto}); + res.push({ depth, propertyNames: properties, object: proto }); proto = Object.getPrototypeOf(proto); } return res; -} +}; /** * Finds a property along the prototype chain @@ -373,64 +378,69 @@ Object.findPropertyInChain = function (subject, propertyName) { let depth = 0; while (subject !== null) { properties = Object.getOwnPropertyNames(subject); - if (properties.includes(propertyName)){ - return {"depth": depth, "propertyName":propertyName}; + if (properties.includes(propertyName)) { + return { depth, propertyName }; } depth++; subject = Object.getPrototypeOf(subject); } throw Error("Property not found. Check whether configuration is correct!"); -} - +}; /* * Get all keys for properties that shall be overwritten */ -function getPropertyKeysToOverwrite(item){ - let res = []; - item.logSettings.overwrittenProperties.forEach(obj =>{ +function getPropertyKeysToOverwrite(item) { + const res = []; + item.logSettings.overwrittenProperties.forEach((obj) => { res.push(obj.key); - }) + }); return res; } function getContextualPrototypeFromString(context, objectAsString) { const obj = context[objectAsString]; if (obj) { - return (obj.prototype) ? obj.prototype : Object.getPrototypeOf(obj); + return obj.prototype ? obj.prototype : Object.getPrototypeOf(obj); } else { return undefined; } } - /** * Prepares a list of properties that need to be instrumented * Here, this can be a previous created list (settings.js: propertiesToInstrument) * or all properties of a given object (settings.js: propertiesToInstrument is empty) */ -function getObjectProperties(context, item){ +function getObjectProperties(context, item) { let propertiesToInstrument = item.logSettings.propertiesToInstrument; - const proto = getContextualPrototypeFromString(context, item["object"]); + const proto = getContextualPrototypeFromString(context, item.object); if (!proto) { - throw Error("Object " + item['object'] + " was undefined."); + throw Error("Object " + item.object + " was undefined."); } if (propertiesToInstrument === undefined || !propertiesToInstrument.length) { - propertiesToInstrument = Object.getPropertyNamesPerDepth(proto, item['depth']); + propertiesToInstrument = Object.getPropertyNamesPerDepth(proto, item.depth); // filter excluded and overwritten properties - const excluded = getPropertyKeysToOverwrite(item).concat(item.logSettings.excludedProperties); - propertiesToInstrument = filterPropertiesPerDepth(propertiesToInstrument, excluded); - }else{ + const excluded = getPropertyKeysToOverwrite(item).concat( + item.logSettings.excludedProperties, + ); + propertiesToInstrument = filterPropertiesPerDepth( + propertiesToInstrument, + excluded, + ); + } else { // include the object to each item - propertiesToInstrument.forEach(propertyList => { - propertyList["object"] = Object.getPrototypeByDepth(proto, propertyList["depth"]); + propertiesToInstrument.forEach((propertyList) => { + propertyList.object = Object.getPrototypeByDepth( + proto, + propertyList.depth, + ); }); } return propertiesToInstrument; } - /* * Enables communication with a background script * Must be injected in a private scope to the @@ -438,18 +448,20 @@ function getObjectProperties(context, item){ * * @param details: property access details */ -function notify(type, content){ +function notify(type, content) { content.timeStamp = new Date().toISOString(); browser.runtime.sendMessage({ namespace: "javascript-instrumentation", type, - data: content + data: content, }); } -function filterPropertiesPerDepth(collection, excluded){ - for (let i=0; i !excluded.includes(p)); +function filterPropertiesPerDepth(collection, excluded) { + for (let i = 0; i < collection.length; i++) { + collection[i].propertyNames = collection[i].propertyNames.filter( + (p) => !excluded.includes(p), + ); } return collection; } @@ -461,10 +473,13 @@ function filterPropertiesPerDepth(collection, excluded){ * @param context: target DOM * @param name: Name of the function (e.g., get width) */ -function exportCustomFunction(func, context, name){ - const targetObject = context.wrappedJSObject.Object.create(null); - const exportedTry = exportFunction(func, targetObject, {allowCrossOriginArguments: true, defineAs: name}); - return exportedTry; +function exportCustomFunction(func, context, name) { + const targetObject = context.wrappedJSObject.Object.create(null); + const exportedTry = exportFunction(func, targetObject, { + allowCrossOriginArguments: true, + defineAs: name, + }); + return exportedTry; } /* @@ -476,117 +491,147 @@ function injectFunction( descriptor, functionType, pageObject, - propertyName){ - const exportedFunction = exportCustomFunction(instrumentedFunction, window, propertyName); - changeProperty(descriptor, pageObject, propertyName, functionType, exportedFunction); + propertyName, +) { + const exportedFunction = exportCustomFunction( + instrumentedFunction, + window, + propertyName, + ); + changeProperty( + descriptor, + pageObject, + propertyName, + functionType, + exportedFunction, + ); } - /* - * Add notifications when a property is requested + * Add notifications when a property is requested * TODO: Bring everything together at this point * * @param original: the original getter/setter function * @param object: * @param args: */ -function instrumentGetObjectProperty(identifier, original, newValue, object, args){ +function instrumentGetObjectProperty( + identifier, + original, + newValue, + object, + args, +) { const originalValue = original.call(object, ...args); const callContext = getOriginatingScriptContext(true); - const returnValue = (newValue !== undefined) ? newValue : originalValue; + const returnValue = newValue !== undefined ? newValue : originalValue; logValue( identifier, returnValue, JSOperation.get, callContext, - //logSettings - ); + // logSettings + ); return returnValue; } - /* - * Add notifications when a property is set - * - * @param original: the original getter/setter function - * @param object: - * @param args: - */ -function instrumentSetObjectProperty(identifier, original, newValue, object, args){ +/* + * Add notifications when a property is set + * + * @param original: the original getter/setter function + * @param object: + * @param args: + */ +function instrumentSetObjectProperty( + identifier, + original, + newValue, + object, + args, +) { const callContext = getOriginatingScriptContext(true); logValue( - identifier, - newValue, - (!!original) ? JSOperation.set : JSOperation.set_failed, - callContext, - //logSettings - ); - if (!original){ - return newValue; - } - else{ - return original.call(object, newValue); - } + identifier, + newValue, + original ? JSOperation.set : JSOperation.set_failed, + callContext, + // logSettings + ); + return !original ? newValue : original.call(object, newValue); } /* -* Creates a getter function -* -* @param descriptor: the descriptor of the original function -* @param funcName: Name of property/function that shall be overwritten -* @param newValue: in Case the value shall be changed -*/ -function generateGetter(identifier, descriptor, propertyName, newValue = undefined){ + * Creates a getter function + * + * @param descriptor: the descriptor of the original function + * @param funcName: Name of property/function that shall be overwritten + * @param newValue: in Case the value shall be changed + */ +function generateGetter( + identifier, + descriptor, + propertyName, + newValue = undefined, +) { const original = descriptor.get; return Object.getOwnPropertyDescriptor( - { - get [propertyName](){ - return instrumentGetObjectProperty(identifier, original, newValue, this, arguments); - } - }, - propertyName).get; + { + get [propertyName]() { + return instrumentGetObjectProperty( + identifier, + original, + newValue, + this, + arguments, + ); + }, + }, + propertyName, + ).get; } /* -* Creates a setter function -* -* @param descriptor: the descriptor of the original function -* @param funcName: Name of property/function that shall be overwritten -* @param newValue: in Case the value shall be changed -*/ -function generateSetter(identifier, descriptor, propertyName, newValue){ + * Creates a setter function + * + * @param descriptor: the descriptor of the original function + * @param funcName: Name of property/function that shall be overwritten + * @param newValue: in Case the value shall be changed + */ +function generateSetter(identifier, descriptor, propertyName, newValue) { const original = descriptor.set; return Object.getOwnPropertyDescriptor( - { - set [propertyName](newValue){ - return instrumentSetObjectProperty(identifier, original, newValue, this, arguments); - } - }, - propertyName).set; + { + set [propertyName](newValue) { + return instrumentSetObjectProperty( + identifier, + original, + newValue, + this, + arguments, + ); + }, + }, + propertyName, + ).set; } /* * Overwrites the prototype to access a property * @param */ -function changeProperty( - descriptor, - pageObject, - name, - method, - changed){ - descriptor[method] = changed; - Object.defineProperty(pageObject, name, descriptor); +function changeProperty(descriptor, pageObject, name, method, changed) { + descriptor[method] = changed; + Object.defineProperty(pageObject, name, descriptor); } - /* * Retrieves an object in a context * * @param context: the window object that is currently instrumented * @param object: the subobject needed */ -function getPageObjectInContext(context, context_object){ - if (context === undefined || context_object === undefined){ - return ; +function getPageObjectInContext(context, context_object) { + if (context === undefined || context_object === undefined) { + return; } return context[context_object].prototype || context[context_object]; } @@ -594,8 +639,7 @@ function getPageObjectInContext(context, context_object){ /* * TODO: Add description */ -const getPropertyType = (object, property) => typeof(object[property]); - +const getPropertyType = (object, property) => typeof object[property]; /* * Entry point to creates (g/s)etter functions, @@ -607,133 +651,188 @@ function instrumentGetterSetter( identifier, pageObject, propertyName, - newValue = undefined) - { - let instrumentedFunction; - const getFuncType = "get"; - const setFuncType = "set"; - - if (descriptor.hasOwnProperty(getFuncType)){ - instrumentedFunction = generateGetter(identifier, descriptor, propertyName, newValue); - injectFunction(instrumentedFunction, descriptor, getFuncType, pageObject, propertyName); - } - if (descriptor.hasOwnProperty(setFuncType)){ - instrumentedFunction = generateSetter(identifier, descriptor, propertyName); - injectFunction(instrumentedFunction, descriptor, setFuncType, pageObject, propertyName); - } + newValue = undefined, +) { + let instrumentedFunction; + const getFuncType = "get"; + const setFuncType = "set"; + + if (descriptor.hasOwnProperty(getFuncType)) { + instrumentedFunction = generateGetter( + identifier, + descriptor, + propertyName, + newValue, + ); + injectFunction( + instrumentedFunction, + descriptor, + getFuncType, + pageObject, + propertyName, + ); + } + if (descriptor.hasOwnProperty(setFuncType)) { + instrumentedFunction = generateSetter(identifier, descriptor, propertyName); + injectFunction( + instrumentedFunction, + descriptor, + setFuncType, + pageObject, + propertyName, + ); + } } /* * TODO: Add description */ function functionGenerator(context, identifier, original, funcName) { - function temp(){ + function temp() { let result; const callContext = getOriginatingScriptContext(true, true); - logCall(identifier, - arguments, - callContext - ); - try{ - result = (arguments.length > 0) ? original.call(this, ...arguments) : original.call(this); - }catch(err){ - let fakeError = generateErrorObject(err); + logCall(identifier, arguments, callContext); + try { + result = + arguments.length > 0 + ? original.call(this, ...arguments) + : original.call(this); + } catch (err) { + const fakeError = generateErrorObject(err); throw fakeError; } return result; } - return temp + return temp; } - /* * TODO: Add description */ -function instrumentFunction(context, descriptor, identifier, pageObject, propertyName) { +function instrumentFunction( + context, + descriptor, + identifier, + pageObject, + propertyName, +) { const original = descriptor.value; - const tempFunction = functionGenerator(context, identifier, original, propertyName); - const exportedFunction = exportCustomFunction(tempFunction, context, original.name); - changeProperty(descriptor, pageObject, propertyName, "value", exportedFunction); + const tempFunction = functionGenerator( + context, + identifier, + original, + propertyName, + ); + const exportedFunction = exportCustomFunction( + tempFunction, + context, + original.name, + ); + changeProperty( + descriptor, + pageObject, + propertyName, + "value", + exportedFunction, + ); } - /* * Helper class to perform all needed functionality * * @param context: the window object that is currently instrumented * @param object: child object that shall be instumented */ -function instrument(context, item, depth, propertyName, newValue = undefined){ - try{ - const identifier = item["instrumentedName"] + "." + propertyName; - const initialPageObject = getPageObjectInContext(context.wrappedJSObject, item["object"]); +function instrument(context, item, depth, propertyName, newValue = undefined) { + try { + const identifier = item.instrumentedName + "." + propertyName; + const initialPageObject = getPageObjectInContext( + context.wrappedJSObject, + item.object, + ); const pageObject = Object.getPrototypeByDepth(initialPageObject, depth); const descriptor = Object.getPropertyDescriptor(pageObject, propertyName); - if (descriptor === undefined){ + if (descriptor === undefined) { // Do not do undefined descriptor. We can safely skip them return; } - if (typeof(descriptor.value) === "function"){ - instrumentFunction(context, descriptor, identifier, pageObject, propertyName); + if (typeof descriptor.value === "function") { + instrumentFunction( + context, + descriptor, + identifier, + pageObject, + propertyName, + ); } else { - instrumentGetterSetter(descriptor, identifier, pageObject, propertyName, newValue); + instrumentGetterSetter( + descriptor, + identifier, + pageObject, + propertyName, + newValue, + ); } - } catch (error){ + } catch (error) { console.error(error); console.error(error.stack); return; } } - /* * Checks if an object was already wrapped * Unwrapped objects should be wrapped immediately */ -let wrappedObjects = []; +const wrappedObjects = []; function needsWrapper(object) { - if (wrappedObjects.some(obj => object === obj)){ + if (wrappedObjects.some((obj) => object === obj)) { return false; } wrappedObjects.push(object); return true; } -function startInstrument(context){ +function startInstrument(context) { for (const item of jsInstrumentationSettings) { - // retrieve Object properties alont the chain let propertyCollection; try { propertyCollection = getObjectProperties(context, item); - } catch (err){ + } catch (err) { console.error(err); continue; } // Instrument each Property per object/prototype - if (propertyCollection[0] !== ""){ - propertyCollection.forEach(({depth, propertyNames, object}) => { - if (needsWrapper(object)){ - propertyNames.forEach(propertyName => instrument(context, item, depth, propertyName)); + if (propertyCollection[0] !== "") { + propertyCollection.forEach(({ depth, propertyNames, object }) => { + if (needsWrapper(object)) { + propertyNames.forEach((propertyName) => + instrument(context, item, depth, propertyName), + ); } }); } // Instrument properties and overwrite their return value - if (item.logSettings.overwrittenProperties){ - item.logSettings.overwrittenProperties.forEach(({key: name, value}) => { - const proto = getContextualPrototypeFromString(context, item["object"]); - if (proto){ - let {depth, propertyName} = Object.findPropertyInChain(proto, name); + if (item.logSettings.overwrittenProperties) { + item.logSettings.overwrittenProperties.forEach(({ key: name, value }) => { + const proto = getContextualPrototypeFromString(context, item.object); + if (proto) { + const { depth, propertyName } = Object.findPropertyInChain( + proto, + name, + ); instrument(context, item, depth, propertyName, value); } else { - console.error("Could not instrument " + item["object"] + ". Encountered undefined object."); + console.error( + "Could not instrument " + + item.object + + ". Encountered undefined object.", + ); } }); } } } -export { - startInstrument, - exportCustomFunction -}; \ No newline at end of file +export { startInstrument, exportCustomFunction }; diff --git a/Extension/src/stealth/settings.ts b/Extension/src/stealth/settings.ts index 06d7c8888..a81accb3c 100644 --- a/Extension/src/stealth/settings.ts +++ b/Extension/src/stealth/settings.ts @@ -1,242 +1,306 @@ export const jsInstrumentationSettings = [ - {"object":"ScriptProcessorNode",// Depcrecated. Replaced by AudioWorkletNode - "instrumentedName":"ScriptProcessorNode", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "ScriptProcessorNode", // Depcrecated. Replaced by AudioWorkletNode + instrumentedName: "ScriptProcessorNode", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"AudioWorkletNode", - "instrumentedName":"AudioWorkletNode", - "depth":1, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "AudioWorkletNode", + instrumentedName: "AudioWorkletNode", + depth: 1, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"GainNode", - "instrumentedName":"GainNode", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "GainNode", + instrumentedName: "GainNode", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"AnalyserNode", - "instrumentedName":"AnalyserNode", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "AnalyserNode", + instrumentedName: "AnalyserNode", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"OscillatorNode", - "instrumentedName":"OscillatorNode", - "depth":1, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "OscillatorNode", + instrumentedName: "OscillatorNode", + depth: 1, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, // Add shared prototype by AnalyserNode, OscillatorNode, ScriptProcessorNode, GainNode, ScriptProcessorNode - {"object":"AnalyserNode", - "instrumentedName":"Node", - "depth":1, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "AnalyserNode", + instrumentedName: "Node", + depth: 1, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"OfflineAudioContext", - "instrumentedName":"OfflineAudioContext", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "OfflineAudioContext", + instrumentedName: "OfflineAudioContext", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"AudioContext", - "instrumentedName":"AudioContext", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "AudioContext", + instrumentedName: "AudioContext", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, -// Add shared prototype by AudioContenxt/OfflineAudioContext - {"object":"AudioContext", - "instrumentedName":"[AudioContenxt|OfflineAudioContext]", - "depth":1, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + // Add shared prototype by AudioContenxt/OfflineAudioContext + { + object: "AudioContext", + instrumentedName: "[AudioContenxt|OfflineAudioContext]", + depth: 1, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"RTCPeerConnection", - "instrumentedName":"RTCPeerConnection", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "RTCPeerConnection", + instrumentedName: "RTCPeerConnection", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"HTMLCanvasElement", - "instrumentedName":"HTMLCanvasElement", - "depth":1, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":["style", "offsetWidth", "offsetHeight"], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "HTMLCanvasElement", + instrumentedName: "HTMLCanvasElement", + depth: 1, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: ["style", "offsetWidth", "offsetHeight"], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"Storage", - "instrumentedName":"Storage", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "Storage", + instrumentedName: "Storage", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"Navigator", - "instrumentedName":"Navigator", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[{"key":"webdriver", "value":false, "level":0}], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "Navigator", + instrumentedName: "Navigator", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [{ key: "webdriver", value: false, level: 0 }], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"CanvasRenderingContext2D", - "instrumentedName":"CanvasRenderingContext2D", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[ - "transform", - "globalAlpha", - "clearRect", - "closePath", - "canvas", - "quadraticCurveTo", - "lineTo", - "moveTo", - "setTransform", - "drawImage", - "beginPath", - "translate"], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "CanvasRenderingContext2D", + instrumentedName: "CanvasRenderingContext2D", + depth: 0, + logSettings: { + propertiesToInstrument: [], + nonExistingPropertiesToInstrument: [], + excludedProperties: [ + "transform", + "globalAlpha", + "clearRect", + "closePath", + "canvas", + "quadraticCurveTo", + "lineTo", + "moveTo", + "setTransform", + "drawImage", + "beginPath", + "translate", + ], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"Screen", - "instrumentedName":"Screen", - "depth":0, - "logSettings":{"propertiesToInstrument":[], - // in OpenWPM is only this one used: - //{"depth":0, "propertyNames":["colorDepth","pixelDepth" - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":false, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, + { + object: "Screen", + instrumentedName: "Screen", + depth: 0, + logSettings: { + propertiesToInstrument: [], + // in OpenWPM is only this one used: + // {"depth":0, "propertyNames":["colorDepth","pixelDepth" + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: false, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, - {"object":"document", - "instrumentedName":"document", - "depth":0, - "logSettings":{"propertiesToInstrument":[{"depth":2, "propertyNames":["referrer"]}], - "nonExistingPropertiesToInstrument":[], - "excludedProperties":[], - "overwrittenProperties":[], - "logCallStack":true, - "logFunctionsAsStrings":false, - "logFunctionGets":false, - "preventSets":false, - "recursive":false, - "depth":5}}, - -]; \ No newline at end of file + { + object: "document", + instrumentedName: "document", + depth: 0, + logSettings: { + propertiesToInstrument: [{ depth: 2, propertyNames: ["referrer"] }], + nonExistingPropertiesToInstrument: [], + excludedProperties: [], + overwrittenProperties: [], + logCallStack: true, + logFunctionsAsStrings: false, + logFunctionGets: false, + preventSets: false, + recursive: false, + depth: 5, + }, + }, +]; diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts index 602ad0655..bd04989a9 100644 --- a/Extension/src/stealth/stealth.ts +++ b/Extension/src/stealth/stealth.ts @@ -4,8 +4,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { startInstrument as instrument, - exportCustomFunction } from "./instrument"; +import { + startInstrument as instrument, + exportCustomFunction, +} from "./instrument"; // Declaring some local trackers const interceptedWindows = new WeakMap(); @@ -13,9 +15,9 @@ const proxies = new Map(); const changedToStrings = new WeakMap(); // Entry point for this extension -(function(){ +(function () { // console.log("Starting frame script"); - try{ + try { interceptWindow(window); } catch (error) { console.log("Instrumentation initialisation crashed. Reason: " + error); @@ -24,358 +26,393 @@ const changedToStrings = new WeakMap(); // console.log("Starting frame script"); })(); +function interceptWindow( + context: Window & typeof globalThis & { wrappedJSObject: any }, +) { + let wrappedTry; + try { + wrappedTry = getWrapped(context); + } catch (error) { + // we are unable to read the location due to SOP + // therefore we also can not intercept anything. + // console.log("NOT intercepting window due to SOP: ", context); + return false; + } + const wrappedWindow = wrappedTry; -function interceptWindow(context){ - let wrappedTry; - try { - const href = context.location.href; - wrappedTry = getWrapped(context); - } catch (error){ - // we are unable to read the location due to SOP - // therefore we also can not intercept anything. - //console.log("NOT intercepting window due to SOP: ", context); - return false; - } - const wrappedWindow = wrappedTry; - - if (interceptedWindows.get(wrappedWindow)){ - //console.log("Already intercepted: ", context); - return false; - } - // console.log("intercepting window", context); + if (interceptedWindows.get(wrappedWindow)) { + // console.log("Already intercepted: ", context); + return false; + } + // console.log("intercepting window", context); instrument(context); interceptedWindows.set(wrappedWindow, true); - //console.log("prepare to intercept "+ context.length +" (i)frames."); - function interceptAllFrames(){ - const currentLength = context.length; - for (let i = currentLength; i--;){ - if (!interceptedWindows.get(wrappedWindow[i])){ - interceptWindow(context[i]); - } - } - } + // console.log("prepare to intercept "+ context.length +" (i)frames."); + function interceptAllFrames() { + const currentLength = context.length; + for (let i = currentLength; i--; ) { + if (!interceptedWindows.get(wrappedWindow[i])) { + interceptWindow(context[i]); + } + } + } protectAllFrames(context, wrappedWindow, interceptWindow, interceptAllFrames); return true; } -function protectAllFrames(context, wrappedWindow, singleCallback, allCallback){ - const changeWindowProperty = createChangeProperty(context); - if (!changeWindowProperty){ - return; - } +function protectAllFrames(context, wrappedWindow, singleCallback, allCallback) { + const changeWindowProperty = createChangeProperty(context); + if (!changeWindowProperty) { + return; + } - const api = {context, wrappedWindow, changeWindowProperty, singleCallback, allCallback}; + const api = { + context, + wrappedWindow, + changeWindowProperty, + singleCallback, + allCallback, + }; - protectFrameProperties(api); + protectFrameProperties(api); - protectDOMModifications(api); + protectDOMModifications(api); - // MutationObserver to intercept iFrames while generating the DOM. - api.observe = enableMutationObserver(api); + // MutationObserver to intercept iFrames while generating the DOM. + api.observe = enableMutationObserver(api); - // MutationObserver does not trigger fast enough when document.write is used - protectDocumentWrite(api); + // MutationObserver does not trigger fast enough when document.write is used + protectDocumentWrite(api); - protectWindowOpen(api); - }; + protectWindowOpen(api); +} -function getWrapped(context) { +function getWrapped( + context: Window & typeof globalThis & { wrappedJSObject: any }, +) { return context && (context.wrappedJSObject || context); } - -function createChangeProperty(window){ - const changeWindowProperty = function (object, name, type, changed){ - const descriptor = Object.getOwnPropertyDescriptor(object, name); - const original = descriptor[type]; - if ((typeof changed) === "function"){ - changed = createProxyFunction(window, original, changed); - } - changePropertyFunc(window, {object, name, type, changed}); - } - return changeWindowProperty; +function createChangeProperty(window) { + const changeWindowProperty = function (object, name, type, changed) { + const descriptor = Object.getOwnPropertyDescriptor(object, name); + const original = descriptor[type]; + if (typeof changed === "function") { + changed = createProxyFunction(window, original, changed); + } + changePropertyFunc(window, { object, name, type, changed }); + }; + return changeWindowProperty; } -function createProxyFunction(context, original, replacement){ - if (!changedToStrings.get(context)){ - changedToStrings.set(context, true); - const functionPrototype = getWrapped(context).Function.prototype; - const toString = functionPrototype.toString; - changePropertyFunc( - context, - { - object: functionPrototype, - name: "toString", - type: "value", - changed: createProxyFunction( - context, - toString, - function(){ - return proxies.get(this) || toString.call(this); - } - ) +function createProxyFunction(context, original, replacement) { + if (!changedToStrings.get(context)) { + changedToStrings.set(context, true); + const functionPrototype = getWrapped(context).Function.prototype; + const toString = functionPrototype.toString; + changePropertyFunc(context, { + object: functionPrototype, + name: "toString", + type: "value", + changed: createProxyFunction(context, toString, function () { + return proxies.get(this) || toString.call(this); + }), + }); + } + const handler = getWrapped(context).Object.create(null); + handler.apply = exportCustomFunction( + function (target, thisArgs, args) { + try { + return args.length + ? replacement.call(thisArgs, ...args) + : replacement.call(thisArgs); + } catch (error) { + try { + return original.apply(thisArgs, args); + } catch (error) { + return target.apply(thisArgs, args); + } } - ); - } - const handler = getWrapped(context).Object.create(null); - handler.apply = exportCustomFunction(function(target, thisArgs, args){ - try { - return args.length? - replacement.call(thisArgs, ...args): - replacement.call(thisArgs); - } - catch (error){ - try { - return original.apply(thisArgs, args); - } - catch (error){ - return target.apply(thisArgs, args); - } - } - }, context, ""); - const proxy = new context.Proxy(original, handler); - proxies.set(proxy, original.toString()); - return getWrapped(proxy); -}; + }, + context, + "", + ); + const proxy = new context.Proxy(original, handler); + proxies.set(proxy, original.toString()); + return getWrapped(proxy); +} -function changePropertyFunc(context, {object, name, type, changed}){ +function changePropertyFunc(context, { object, name, type, changed }) { // Removed tracker for changed properties - const descriptor = Object.getOwnPropertyDescriptor(object, name); - const original = descriptor[type]; - descriptor[type] = changed; - Object.defineProperty(object, name, descriptor); -}; - + const descriptor = Object.getOwnPropertyDescriptor(object, name); + descriptor[type] = changed; + Object.defineProperty(object, name, descriptor); +} -function protectFrameProperties({context, wrappedWindow, changeWindowProperty, singleCallback}){ - ["HTMLIFrameElement", "HTMLFrameElement"].forEach(function(constructorName){ +function protectFrameProperties({ + context, + wrappedWindow, + changeWindowProperty, + singleCallback, +}) { + ["HTMLIFrameElement", "HTMLFrameElement"].forEach(function (constructorName) { const constructor = context[constructorName]; const wrappedConstructor = wrappedWindow[constructorName]; const contentWindowDescriptor = Object.getOwnPropertyDescriptor( constructor.prototype, - "contentWindow" + "contentWindow", ); - //TODO: Continue here!!!! + // TODO: Continue here!!!! const originalContentWindowGetter = contentWindowDescriptor.get; const contentWindowTemp = { - get contentWindow(){ + get contentWindow() { const window = originalContentWindowGetter.call(this); - if (window){ + if (window) { singleCallback(window); } return window; - } + }, }; - changeWindowProperty(wrappedConstructor.prototype, "contentWindow", "get", - Object.getOwnPropertyDescriptor(contentWindowTemp, "contentWindow").get + changeWindowProperty( + wrappedConstructor.prototype, + "contentWindow", + "get", + Object.getOwnPropertyDescriptor(contentWindowTemp, "contentWindow").get, ); const contentDocumentDescriptor = Object.getOwnPropertyDescriptor( constructor.prototype, - "contentDocument" + "contentDocument", ); const originalContentDocumentGetter = contentDocumentDescriptor.get; const contentDocumentTemp = { - get contentDocument(){ + get contentDocument() { const document = originalContentDocumentGetter.call(this); - if (document){ + if (document) { singleCallback(document.defaultView); } return document; - } + }, }; - changeWindowProperty(wrappedConstructor.prototype, "contentDocument", "get", - Object.getOwnPropertyDescriptor(contentDocumentTemp, "contentDocument").get + changeWindowProperty( + wrappedConstructor.prototype, + "contentDocument", + "get", + Object.getOwnPropertyDescriptor(contentDocumentTemp, "contentDocument") + .get, ); }); } -function protectDOMModifications({context, wrappedWindow, changeWindowProperty, allCallback}){ - [ - // useless as length could be obtained before the iframe is created and window.frames === window - // { - // object: wrappedWindow, - // methods: [], - // getters: ["length", "frames"], - // setters: [] - // }, - { - object: wrappedWindow.Node.prototype, - methods: ["appendChild", "insertBefore", "replaceChild"], - getters: [], - setters: [] - }, - { - object: wrappedWindow.Element.prototype, - methods: [ - "append", "prepend", - "insertAdjacentElement", "insertAdjacentHTML", "insertAdjacentText", - "replaceWith" - ], - getters: [], - setters: [ - "innerHTML", - "outerHTML" - ] - } - ].forEach(function(protectionDefinition){ - const object = protectionDefinition.object; - protectionDefinition.methods.forEach(function(method){ - const descriptor = Object.getOwnPropertyDescriptor(object, method); - const original = descriptor.value; - changeWindowProperty(object, method, "value", class { - [method](){ - const value = arguments.length? - original.call(this, ...arguments): - original.call(this); - allCallback(); - return value; - } - }.prototype[method]); - }); - protectionDefinition.getters.forEach(function(property){ - const temp = { - get [property](){ - const ret = this[property]; - allCallback(); - return ret; - } - }; - changeWindowProperty(object, property, "get", - Object.getOwnPropertyDescriptor(temp, property).get - ); - }); - protectionDefinition.setters.forEach(function(property){ - const descriptor = Object.getOwnPropertyDescriptor(object, property); - const setter = descriptor.set; - const temp = { - set [property](value){ - const ret = setter.call(this, value); - allCallback(); - return ret; - } - }; - changeWindowProperty(object, property, "set", - Object.getOwnPropertyDescriptor(temp, property).set - ); - }); - }); +function protectDOMModifications({ + context, + wrappedWindow, + changeWindowProperty, + allCallback, +}) { + [ + // useless as length could be obtained before the iframe is created and window.frames === window + // { + // object: wrappedWindow, + // methods: [], + // getters: ["length", "frames"], + // setters: [] + // }, + { + object: wrappedWindow.Node.prototype, + methods: ["appendChild", "insertBefore", "replaceChild"], + getters: [], + setters: [], + }, + { + object: wrappedWindow.Element.prototype, + methods: [ + "append", + "prepend", + "insertAdjacentElement", + "insertAdjacentHTML", + "insertAdjacentText", + "replaceWith", + ], + getters: [], + setters: ["innerHTML", "outerHTML"], + }, + ].forEach(function (protectionDefinition) { + const object = protectionDefinition.object; + protectionDefinition.methods.forEach(function (method) { + const descriptor = Object.getOwnPropertyDescriptor(object, method); + const original = descriptor.value; + changeWindowProperty( + object, + method, + "value", + class { + [method]() { + const value = arguments.length + ? original.call(this, ...arguments) + : original.call(this); + allCallback(); + return value; + } + }.prototype[method], + ); + }); + protectionDefinition.getters.forEach(function (property) { + const temp = { + get [property]() { + const ret = this[property]; + allCallback(); + return ret; + }, + }; + changeWindowProperty( + object, + property, + "get", + Object.getOwnPropertyDescriptor(temp, property).get, + ); + }); + protectionDefinition.setters.forEach(function (property) { + const descriptor = Object.getOwnPropertyDescriptor(object, property); + const setter = descriptor.set; + const temp = { + set [property](value) { + const ret = setter.call(this, value); + allCallback(); + return ret; + }, + }; + changeWindowProperty( + object, + property, + "set", + Object.getOwnPropertyDescriptor(temp, property).set, + ); + }); + }); } -function enableMutationObserver({context, allCallback}){ - const observer = new MutationObserver(allCallback); - let observing = false; - function observe(){ - if ( - !observing && - context.document - ){ - observer.observe(context.document, {subtree: true, childList: true}); - observing = true; - } - } - observe(); - context.document.addEventListener("DOMContentLoaded", function(){ - if (observing){ - observer.disconnect(); - observing = false; - } - }); - return observe; +function enableMutationObserver({ context, allCallback }) { + const observer = new MutationObserver(allCallback); + let observing = false; + function observe() { + if (!observing && context.document) { + observer.observe(context.document, { subtree: true, childList: true }); + observing = true; + } + } + observe(); + context.document.addEventListener("DOMContentLoaded", function () { + if (observing) { + observer.disconnect(); + observing = false; + } + }); + return observe; } -function protectDocumentWrite({context, wrappedWindow, changeWindowProperty, observe, allCallback}){ - const documentWriteDescriptorOnHTMLDocument = Object.getOwnPropertyDescriptor( - wrappedWindow.HTMLDocument.prototype, - "write" - ); - const documentWriteDescriptor = documentWriteDescriptorOnHTMLDocument || Object.getOwnPropertyDescriptor( - wrappedWindow.Document.prototype, - "write" - ); - const documentWrite = documentWriteDescriptor.value; - changeWindowProperty( - documentWriteDescriptorOnHTMLDocument? - wrappedWindow.HTMLDocument.prototype: - wrappedWindow.Document.prototype, - "write", "value", function write(markup){ - for (let i = 0, l = arguments.length; i < l; i += 1){ - const str = "" + arguments[i]; - // weird problem with waterfox and google docs - const parts = ( - str.match(/^\s* Date: Wed, 13 Sep 2023 13:17:11 +0200 Subject: [PATCH 11/16] fix(TS): define functions on Object --- Extension/src/stealth/instrument.ts | 23 ++++++++++++++++------- Extension/src/stealth/stealth.ts | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Extension/src/stealth/instrument.ts b/Extension/src/stealth/instrument.ts index d9018f1bd..e9b7b2db2 100644 --- a/Extension/src/stealth/instrument.ts +++ b/Extension/src/stealth/instrument.ts @@ -237,7 +237,7 @@ function logValue( value, // : any, operation, // : string, // from JSOperation object please callContext, // : any, - logSettings = false, // : LogSettings, + logSettings: typeof jsInstrumentationSettings[0] | { logFunctionsAsStrings: boolean } = { logFunctionsAsStrings: false }, // : LogSettings, ) { if (inLog) { return; @@ -327,7 +327,16 @@ function logCall(instrumentedFunctionName, args, callContext, _logSettings) { /** * Provides the properties per prototype object */ -Object.getPrototypeByDepth = function (subject, depth) { + +declare global { + interface Object { + getPrototypeByDepth(subject: string | undefined, depth: number): any + getPropertyNamesPerDepth(subject: any, maxDepth: number): any + findPropertyInChain(subject, propertyName) + } +} + +Object.prototype.getPrototypeByDepth = function (subject, depth) { if (subject === undefined) { throw new Error("Can't get property names for undefined"); } @@ -348,7 +357,7 @@ Object.getPrototypeByDepth = function (subject, depth) { * Traverses the prototype chain to collect properties. Returns an array containing * an object with the depth, propertyNames and scanned subject */ -Object.getPropertyNamesPerDepth = function (subject, maxDepth = 0) { +Object.prototype.getPropertyNamesPerDepth = function (subject, maxDepth = 0) { if (subject === undefined) { throw new Error("Can't get property names for undefined"); } @@ -370,7 +379,7 @@ Object.getPropertyNamesPerDepth = function (subject, maxDepth = 0) { /** * Finds a property along the prototype chain */ -Object.findPropertyInChain = function (subject, propertyName) { +Object.prototype.findPropertyInChain = function (subject, propertyName) { if (subject === undefined || propertyName === undefined) { throw new Error("Object and property name must be defined"); } @@ -457,9 +466,9 @@ function notify(type, content) { }); } -function filterPropertiesPerDepth(collection, excluded) { - for (let i = 0; i < collection.length; i++) { - collection[i].propertyNames = collection[i].propertyNames.filter( +function filterPropertiesPerDepth(collection: {propertyNames: T[]}[], excluded: T[]) { + for (const elem of collection) { + elem.propertyNames = elem.propertyNames.filter( (p) => !excluded.includes(p), ); } diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts index bd04989a9..5c1404c66 100644 --- a/Extension/src/stealth/stealth.ts +++ b/Extension/src/stealth/stealth.ts @@ -13,12 +13,12 @@ import { const interceptedWindows = new WeakMap(); const proxies = new Map(); const changedToStrings = new WeakMap(); - +export type ModifiedWindow = Window & typeof globalThis & { wrappedJSObject: any }; // Entry point for this extension (function () { // console.log("Starting frame script"); try { - interceptWindow(window); + interceptWindow(window as ModifiedWindow); } catch (error) { console.log("Instrumentation initialisation crashed. Reason: " + error); console.log(error.stack); From 7144f32d3a51e84432d1d7c823eb32d160594b63 Mon Sep 17 00:00:00 2001 From: vringar Date: Thu, 25 Jan 2024 22:18:55 +0100 Subject: [PATCH 12/16] fix(Extension): tsc passes --- Extension/package-lock.json | 279 ++++++++++++++++++ Extension/package.json | 6 +- Extension/src/stealth/error.ts | 2 +- Extension/src/stealth/instrument.ts | 58 ++-- Extension/src/stealth/settings.ts | 4 +- Extension/src/stealth/stealth.ts | 20 +- .../src/types/javascript-instrument.d.ts | 3 + .../src/types/js_instrument_settings.d.ts | 111 +++++++ Extension/webpack.config.js | 1 - schemas/js_instrument_settings.schema.json | 63 +++- 10 files changed, 507 insertions(+), 40 deletions(-) create mode 100644 Extension/src/types/js_instrument_settings.d.ts diff --git a/Extension/package-lock.json b/Extension/package-lock.json index 6043ac50c..4e29f52c7 100644 --- a/Extension/package-lock.json +++ b/Extension/package-lock.json @@ -29,6 +29,7 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-unicorn": "^48.0.1", "express": "^4.18.2", + "json-schema-to-typescript": "^13.1.2", "prettier": "^3.0.3", "safe-compare": "^1.1.4", "ts-loader": "^9.5.0", @@ -1766,6 +1767,24 @@ "node": ">=6.9.0" } }, + "node_modules/@bcherny/json-schema-ref-parser": { + "version": "10.0.5-fork", + "resolved": "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz", + "integrity": "sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@devicefarmer/adbkit": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-3.2.3.tgz", @@ -2099,6 +2118,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, "node_modules/@mdn/browser-compat-data": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.4.3.tgz", @@ -2318,6 +2343,16 @@ "integrity": "sha512-YYE+4MeJvq7DZ+UzPD8c5uN1HJpGu4Fl6O6PEAfBJQmLzQkfTWlgMjZMJQHAmcH3rjVS5fjN+jMkkZ4ZTlKbmA==", "dev": true }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/got": { "version": "9.6.12", "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", @@ -2347,6 +2382,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -2368,6 +2409,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -4231,6 +4278,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4436,6 +4489,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-color": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", + "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.61", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4822,6 +4891,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -5566,12 +5645,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "node_modules/es6-promisify": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", @@ -5581,6 +5686,28 @@ "node": ">=6" } }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6487,6 +6614,16 @@ "node": ">= 0.6" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -6648,6 +6785,15 @@ "node": ">= 0.8" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -6673,6 +6819,12 @@ "node": ">=4" } }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "dev": true + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7248,6 +7400,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -7317,6 +7481,25 @@ "node": ">= 6" } }, + "node_modules/glob-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-4.2.2.tgz", + "integrity": "sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/ahmadnassri" + }, + "peerDependencies": { + "glob": "^7.1.6" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -8248,6 +8431,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8591,6 +8780,49 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, + "node_modules/json-schema-to-typescript": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-13.1.2.tgz", + "integrity": "sha512-17G+mjx4nunvOpkPvcz7fdwUwYCEwyH8vR3Ym3rFiQ8uzAL3go+c1306Kk7iGRk8HuXBXqy+JJJmpYl0cvOllw==", + "dev": true, + "dependencies": { + "@bcherny/json-schema-ref-parser": "10.0.5-fork", + "@types/json-schema": "^7.0.11", + "@types/lodash": "^4.14.182", + "@types/prettier": "^2.6.1", + "cli-color": "^2.0.2", + "get-stdin": "^8.0.0", + "glob": "^7.1.6", + "glob-promise": "^4.2.2", + "is-glob": "^4.0.3", + "lodash": "^4.17.21", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "mz": "^2.7.0", + "prettier": "^2.6.2" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -8923,6 +9155,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -9028,6 +9269,22 @@ "node": ">=6" } }, + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -9357,6 +9614,12 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -12639,6 +12902,16 @@ "node": ">=0.10.0" } }, + "node_modules/timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dev": true, + "dependencies": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -12951,6 +13224,12 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/Extension/package.json b/Extension/package.json index cd179d3a9..4ce72855e 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -38,7 +38,8 @@ "typescript": "^5.2.2", "web-ext": "^7.8.0", "webpack": "^5.88.2", - "webpack-cli": "^5.1.4" + "webpack-cli": "^5.1.4", + "json-schema-to-typescript": "^13.1.2" }, "engines": { "node": ">=14.0.0" @@ -60,7 +61,8 @@ "lint:web-ext-lint": "web-ext lint --privileged", "lint:prettier": "prettier --list-different .", "info": "npm-scripts-info", - "build": "npm run clean && npm run build:main && npm run build:module && npm run build:webpack && npm run build:webext", + "build": "npm run clean && npm run build:generate_from_schema && npm run build:main && npm run build:module && npm run build:webpack && npm run build:webext", + "build:generate_from_schema": "json2ts --input ../schemas/js_instrument_settings.schema.json --output src/types/js_instrument_settings.d.ts", "build:main": "tsc -p tsconfig.json", "build:module": "tsc -p tsconfig.module.json", "build:webpack": "webpack", diff --git a/Extension/src/stealth/error.ts b/Extension/src/stealth/error.ts index 7f45d2c5a..20185fb35 100644 --- a/Extension/src/stealth/error.ts +++ b/Extension/src/stealth/error.ts @@ -1,7 +1,7 @@ /* * Functionality to generate error objects */ -function generateErrorObject(err, context) { +function generateErrorObject(err: { stack: any; name: string | number; message: any; }, context = undefined) { // TODO: Pass context context = context !== undefined ? context : window; const cleaned = cleanErrorStack(err.stack); diff --git a/Extension/src/stealth/instrument.ts b/Extension/src/stealth/instrument.ts index e9b7b2db2..5826ec1df 100644 --- a/Extension/src/stealth/instrument.ts +++ b/Extension/src/stealth/instrument.ts @@ -4,15 +4,17 @@ import { getBeginOfScriptCalls, getStackTrace, } from "./error"; +import {LogSettings} from "../types/js_instrument_settings"; + /** ************************************ * OpenWPM legacy code ***************************************/ // Counter to cap # of calls logged for each script/api combination const maxLogCount = 500; // logCounter -const logCounter = new Object(); +const logCounter = {}; // Prevent logging of gets arising from logging -let inLog = false; +let inLog :boolean = false; // To keep track of the original order of events let ordinal = 0; @@ -28,7 +30,7 @@ const JSOperation = { }; // from http://stackoverflow.com/a/5202185 -function rsplit(source, sep, maxsplit) { +function rsplit(source: string, sep: string, maxsplit: number) { const split = source.split(sep); return maxsplit ? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit)) @@ -169,7 +171,7 @@ function getOriginatingScriptContext(getCallStack = false, isCall = false) { // If not included, use heuristic, 0-3 or 0-2 are OpenWPMs functions traceStart = isCall ? 3 : 4; } - const callSite = trace[traceStart]; + const callSite: string | null = trace[traceStart]; if (!callSite) { return empty_context; } @@ -231,13 +233,23 @@ function getOriginatingScriptContext(getCallStack = false, isCall = false) { // } // } +; // For gets, sets, etc. on a single value function logValue( instrumentedVariableName, // : string, value, // : any, operation, // : string, // from JSOperation object please callContext, // : any, - logSettings: typeof jsInstrumentationSettings[0] | { logFunctionsAsStrings: boolean } = { logFunctionsAsStrings: false }, // : LogSettings, + logSettings: LogSettings = { + depth: 0, + excludedProperties: [], + logCallStack: false, + logFunctionGets: false, + nonExistingPropertiesToInstrument: [], + preventSets: false, + propertiesToInstrument: [], + recursive: false, + logFunctionsAsStrings: false }, // : LogSettings, ) { if (inLog) { return; @@ -278,7 +290,7 @@ function logValue( } // For functions -function logCall(instrumentedFunctionName, args, callContext, _logSettings) { +function logCall(instrumentedFunctionName, args, callContext) { if (inLog) { return; } @@ -330,9 +342,9 @@ function logCall(instrumentedFunctionName, args, callContext, _logSettings) { declare global { interface Object { - getPrototypeByDepth(subject: string | undefined, depth: number): any - getPropertyNamesPerDepth(subject: any, maxDepth: number): any - findPropertyInChain(subject, propertyName) + getPrototypeByDepth(subject: string | undefined, depth: number): any; + getPropertyNamesPerDepth(subject: any, maxDepth: number): any; + findPropertyInChain(subject, propertyName); } } @@ -466,9 +478,12 @@ function notify(type, content) { }); } -function filterPropertiesPerDepth(collection: {propertyNames: T[]}[], excluded: T[]) { +function filterPropertiesPerDepth( + collection: { propertyNames: T[] }[], + excluded: T[], +) { for (const elem of collection) { - elem.propertyNames = elem.propertyNames.filter( + elem.propertyNames = elem.propertyNames.filter( (p) => !excluded.includes(p), ); } @@ -555,7 +570,7 @@ function instrumentSetObjectProperty( original, newValue, object, - args, + _args, ) { const callContext = getOriginatingScriptContext(true); logValue( @@ -605,19 +620,21 @@ function generateGetter( * @param funcName: Name of property/function that shall be overwritten * @param newValue: in Case the value shall be changed */ -function generateSetter(identifier, descriptor, propertyName, newValue) { +function generateSetter(identifier, descriptor, propertyName, _newValue: any | undefined = undefined) { const original = descriptor.set; return Object.getOwnPropertyDescriptor( { - set [propertyName](newValue) { + set(obj, _prop, value) { + // _prop === propertyName return instrumentSetObjectProperty( identifier, original, - newValue, - this, + value, + obj, arguments, ); - }, + } + }, propertyName, ).set; @@ -645,11 +662,6 @@ function getPageObjectInContext(context, context_object) { return context[context_object].prototype || context[context_object]; } -/* - * TODO: Add description - */ -const getPropertyType = (object, property) => typeof object[property]; - /* * Entry point to creates (g/s)etter functions, * instrument them and inject them to the page @@ -696,7 +708,7 @@ function instrumentGetterSetter( /* * TODO: Add description */ -function functionGenerator(context, identifier, original, funcName) { +function functionGenerator(_context, identifier, original, _funcName) { function temp() { let result; const callContext = getOriginatingScriptContext(true, true); diff --git a/Extension/src/stealth/settings.ts b/Extension/src/stealth/settings.ts index a81accb3c..171ed4a4a 100644 --- a/Extension/src/stealth/settings.ts +++ b/Extension/src/stealth/settings.ts @@ -1,4 +1,6 @@ -export const jsInstrumentationSettings = [ +import { JSInstrumentSettings } from "../types/js_instrument_settings"; + +export const jsInstrumentationSettings: JSInstrumentSettings = [ { object: "ScriptProcessorNode", // Depcrecated. Replaced by AudioWorkletNode instrumentedName: "ScriptProcessorNode", diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts index 5c1404c66..d25d4a601 100644 --- a/Extension/src/stealth/stealth.ts +++ b/Extension/src/stealth/stealth.ts @@ -13,7 +13,8 @@ import { const interceptedWindows = new WeakMap(); const proxies = new Map(); const changedToStrings = new WeakMap(); -export type ModifiedWindow = Window & typeof globalThis & { wrappedJSObject: any }; +export type ModifiedWindow = Window & + typeof globalThis & { wrappedJSObject: any }; // Entry point for this extension (function () { // console.log("Starting frame script"); @@ -27,7 +28,7 @@ export type ModifiedWindow = Window & typeof globalThis & { wrappedJSObject: any })(); function interceptWindow( - context: Window & typeof globalThis & { wrappedJSObject: any }, + context: ModifiedWindow, ) { let wrappedTry; try { @@ -53,7 +54,7 @@ function interceptWindow( const currentLength = context.length; for (let i = currentLength; i--; ) { if (!interceptedWindows.get(wrappedWindow[i])) { - interceptWindow(context[i]); + interceptWindow(context[i] as ModifiedWindow); } } } @@ -73,6 +74,7 @@ function protectAllFrames(context, wrappedWindow, singleCallback, allCallback) { changeWindowProperty, singleCallback, allCallback, + observe: null }; protectFrameProperties(api); @@ -143,7 +145,7 @@ function createProxyFunction(context, original, replacement) { return getWrapped(proxy); } -function changePropertyFunc(context, { object, name, type, changed }) { +function changePropertyFunc(_context, { object, name, type, changed }) { // Removed tracker for changed properties const descriptor = Object.getOwnPropertyDescriptor(object, name); descriptor[type] = changed; @@ -206,8 +208,8 @@ function protectFrameProperties({ }); } + function protectDOMModifications({ - context, wrappedWindow, changeWindowProperty, allCallback, @@ -278,8 +280,8 @@ function protectDOMModifications({ const descriptor = Object.getOwnPropertyDescriptor(object, property); const setter = descriptor.set; const temp = { - set [property](value) { - const ret = setter.call(this, value); + set(obj, _prop, value) { + const ret = setter.call(obj, value); allCallback(); return ret; }, @@ -334,7 +336,7 @@ function protectDocumentWrite({ : wrappedWindow.Document.prototype, "write", "value", - function write(markup) { + function write(_markup) { for (let i = 0, l = arguments.length; i < l; i += 1) { const str = "" + arguments[i]; // weird problem with waterfox and google docs @@ -373,7 +375,7 @@ function protectDocumentWrite({ : wrappedWindow.Document.prototype, "writeln", "value", - function writeln(markup) { + function writeln(_markup) { for (let i = 0, l = arguments.length; i < l; i += 1) { const str = "" + arguments[i]; const parts = str.split(/(?=<)/); diff --git a/Extension/src/types/javascript-instrument.d.ts b/Extension/src/types/javascript-instrument.d.ts index da37369db..b8656adc9 100644 --- a/Extension/src/types/javascript-instrument.d.ts +++ b/Extension/src/types/javascript-instrument.d.ts @@ -8,4 +8,7 @@ declare global { interface Window { openWpmContentScriptConfig: openWpmContentScriptConfig; } + // https://searchfox.org/mozilla-central/source/js/xpconnect/idl/xpccomponents.idl#519-534 + // https://searchfox.org/mozilla-central/rev/1e726a0e49225dc174ab55d1d0b21e86208d7251/js/xpconnect/src/xpcprivate.h#2332-2348 + function exportFunction(vfunction: any, scope: any, options: {defineAs: string, allowCrossOriginArguments: boolean}):any } diff --git a/Extension/src/types/js_instrument_settings.d.ts b/Extension/src/types/js_instrument_settings.d.ts new file mode 100644 index 000000000..fda16d1d5 --- /dev/null +++ b/Extension/src/types/js_instrument_settings.d.ts @@ -0,0 +1,111 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * The JS object to be instrumented. + */ +export type JSObject = string; +/** + * The name recorded by the instrumentation for this object. + */ +export type InstrumentedName = string; +/** + * An array of properties to instrument on this object. If array is empty, then all properties are instrumented. + */ +export type PropertiesToInstrument = + | { + depth?: number; + propertyNames?: string[]; + [k: string]: unknown; + }[] + | null; +/** + * An array of non-existing properties to instrument on this object. + */ +export type NonExistingPropertiesToInstrument = string[]; +/** + * Properties excluded from instrumentation. + */ +export type ExcludedProperties = string[]; +/** + * Set to true save the call stack info with each property call. + */ +export type LogCallStack = boolean; +/** + * Set to true to save args that are functions as strings during argument serialization. If false `FUNCTION` is recorded. + */ +export type LogFunctionsAsStrings = boolean; +/** + * Set true to log get requests to properties that are functions. If true when a call is made, the log will contain both the call and a get log. + */ +export type LogFunctionGets = boolean; +/** + * Set to true to prevent nested objects and functions from being overwritten (and thus having their instrumentation removed). Other properties (static values) can still be set with this is enabled. + */ +export type PreventSets = boolean; +/** + * Set to `true` to recursively instrument all object properties of the given `object`. NOTE: (1) `propertiesToInstrument` does not propagate to sub-objects. (2) Sub-objects of prototypes can not be instrumented recursively as these properties can not be accessed until an instance of the prototype is created. + */ +export type Recursive = boolean; +/** + * Recursion limit when instrumenting object recursively + */ +export type RecursionDepth = number; +/** + * TODO + */ +export type TODO = + | { + /** + * The property that will pe overwritten + */ + key?: string; + /** + * The value that should be returned + */ + value: + | string + | unknown[] + | { + [k: string]: unknown; + } + | boolean + | number + | number; + /** + * TODO + */ + level: number; + [k: string]: unknown; + }[] + | null; +/** + * Schema describing the JSON to be passed to JS Instrument Settings. + */ +export type JSInstrumentSettings = SettingsObjects[]; + +export interface SettingsObjects { + object: JSObject; + instrumentedName: InstrumentedName; + depth?: number; + logSettings: LogSettings; +} +/** + * The log settings object. + */ +export interface LogSettings { + propertiesToInstrument: PropertiesToInstrument; + nonExistingPropertiesToInstrument: NonExistingPropertiesToInstrument; + excludedProperties: ExcludedProperties; + logCallStack: LogCallStack; + logFunctionsAsStrings: LogFunctionsAsStrings; + logFunctionGets: LogFunctionGets; + preventSets: PreventSets; + recursive: Recursive; + depth: RecursionDepth; + overwrittenProperties?: TODO; +} diff --git a/Extension/webpack.config.js b/Extension/webpack.config.js index 3ec3ea7f0..359398f3f 100644 --- a/Extension/webpack.config.js +++ b/Extension/webpack.config.js @@ -5,7 +5,6 @@ module.exports = { feature: "./src/feature.ts", content: "./src/content.ts", stealth: "./src/stealth/stealth.ts", - }, output: { path: path.resolve(__dirname, "bundled"), diff --git a/schemas/js_instrument_settings.schema.json b/schemas/js_instrument_settings.schema.json index 029a51d8a..d3c1c81e2 100644 --- a/schemas/js_instrument_settings.schema.json +++ b/schemas/js_instrument_settings.schema.json @@ -18,6 +18,9 @@ "description": "The name recorded by the instrumentation for this object.", "type": "string" }, + "depth": { + "type": "integer" + }, "logSettings": { "title": "Log settings", "description": "The log settings object.", @@ -27,9 +30,21 @@ "title": "Properties to instrument", "description": "An array of properties to instrument on this object. If array is empty, then all properties are instrumented.", "type": ["array", "null"], - "items": { - "type": "string" - }, + "items": + { + "type": "object", + "properties": { + "depth": { + "type": "integer" + }, + "propertyNames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "default": [] }, "nonExistingPropertiesToInstrument": { @@ -85,6 +100,48 @@ "description": "Recursion limit when instrumenting object recursively", "type": "number", "default": 5 + }, + "overwrittenProperties": { + "title": "TODO", + "description": "TODO", + "type": ["array", "null"], + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The property that will pe overwritten" + }, + "value": { + "description": "The value that should be returned", + "anyOf": [{ + "type": "string" + }, + { + "type": "array" + }, { + "type": "object" + },{ + "type": "boolean" + },{ + "type": "number" + },{ + "type": "integer" + }] + }, + "level": { + "type": "integer", + "description": "TODO" + } + + }, + "required": [ + "type", + "value", + "level" + ] + }, + "default": [] } }, "required": [ From 6edaa4e8f3d0e162ec5c34a5057bbc66d16bbd12 Mon Sep 17 00:00:00 2001 From: vringar Date: Thu, 25 Jan 2024 22:21:06 +0100 Subject: [PATCH 13/16] fix(Extension): ran npm run fix --- Extension/src/stealth/error.ts | 5 ++++- Extension/src/stealth/instrument.ts | 18 +++++++++++------- Extension/src/stealth/stealth.ts | 7 ++----- Extension/src/types/javascript-instrument.d.ts | 6 +++++- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Extension/src/stealth/error.ts b/Extension/src/stealth/error.ts index 20185fb35..f2ccfc7fe 100644 --- a/Extension/src/stealth/error.ts +++ b/Extension/src/stealth/error.ts @@ -1,7 +1,10 @@ /* * Functionality to generate error objects */ -function generateErrorObject(err: { stack: any; name: string | number; message: any; }, context = undefined) { +function generateErrorObject( + err: { stack: any; name: string | number; message: any }, + context = undefined, +) { // TODO: Pass context context = context !== undefined ? context : window; const cleaned = cleanErrorStack(err.stack); diff --git a/Extension/src/stealth/instrument.ts b/Extension/src/stealth/instrument.ts index 5826ec1df..2457ddb93 100644 --- a/Extension/src/stealth/instrument.ts +++ b/Extension/src/stealth/instrument.ts @@ -4,7 +4,7 @@ import { getBeginOfScriptCalls, getStackTrace, } from "./error"; -import {LogSettings} from "../types/js_instrument_settings"; +import { LogSettings } from "../types/js_instrument_settings"; /** ************************************ * OpenWPM legacy code @@ -14,7 +14,7 @@ const maxLogCount = 500; // logCounter const logCounter = {}; // Prevent logging of gets arising from logging -let inLog :boolean = false; +let inLog: boolean = false; // To keep track of the original order of events let ordinal = 0; @@ -233,7 +233,6 @@ function getOriginatingScriptContext(getCallStack = false, isCall = false) { // } // } -; // For gets, sets, etc. on a single value function logValue( instrumentedVariableName, // : string, @@ -249,7 +248,8 @@ function logValue( preventSets: false, propertiesToInstrument: [], recursive: false, - logFunctionsAsStrings: false }, // : LogSettings, + logFunctionsAsStrings: false, + }, // : LogSettings, ) { if (inLog) { return; @@ -620,7 +620,12 @@ function generateGetter( * @param funcName: Name of property/function that shall be overwritten * @param newValue: in Case the value shall be changed */ -function generateSetter(identifier, descriptor, propertyName, _newValue: any | undefined = undefined) { +function generateSetter( + identifier, + descriptor, + propertyName, + _newValue: any | undefined = undefined, +) { const original = descriptor.set; return Object.getOwnPropertyDescriptor( { @@ -633,8 +638,7 @@ function generateSetter(identifier, descriptor, propertyName, _newValue: any | u obj, arguments, ); - } - + }, }, propertyName, ).set; diff --git a/Extension/src/stealth/stealth.ts b/Extension/src/stealth/stealth.ts index d25d4a601..83d650ab0 100644 --- a/Extension/src/stealth/stealth.ts +++ b/Extension/src/stealth/stealth.ts @@ -27,9 +27,7 @@ export type ModifiedWindow = Window & // console.log("Starting frame script"); })(); -function interceptWindow( - context: ModifiedWindow, -) { +function interceptWindow(context: ModifiedWindow) { let wrappedTry; try { wrappedTry = getWrapped(context); @@ -74,7 +72,7 @@ function protectAllFrames(context, wrappedWindow, singleCallback, allCallback) { changeWindowProperty, singleCallback, allCallback, - observe: null + observe: null, }; protectFrameProperties(api); @@ -208,7 +206,6 @@ function protectFrameProperties({ }); } - function protectDOMModifications({ wrappedWindow, changeWindowProperty, diff --git a/Extension/src/types/javascript-instrument.d.ts b/Extension/src/types/javascript-instrument.d.ts index b8656adc9..f86bca988 100644 --- a/Extension/src/types/javascript-instrument.d.ts +++ b/Extension/src/types/javascript-instrument.d.ts @@ -10,5 +10,9 @@ declare global { } // https://searchfox.org/mozilla-central/source/js/xpconnect/idl/xpccomponents.idl#519-534 // https://searchfox.org/mozilla-central/rev/1e726a0e49225dc174ab55d1d0b21e86208d7251/js/xpconnect/src/xpcprivate.h#2332-2348 - function exportFunction(vfunction: any, scope: any, options: {defineAs: string, allowCrossOriginArguments: boolean}):any + function exportFunction( + vfunction: any, + scope: any, + options: { defineAs: string; allowCrossOriginArguments: boolean }, + ): any; } From cac2fa9cd012baf6722b590382d634eaec103735 Mon Sep 17 00:00:00 2001 From: vringar Date: Tue, 6 Feb 2024 21:17:02 +0100 Subject: [PATCH 14/16] fix(schema): allow unexpanded form in schema --- schemas/js_instrument_settings.schema.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/schemas/js_instrument_settings.schema.json b/schemas/js_instrument_settings.schema.json index d3c1c81e2..e2e65a52e 100644 --- a/schemas/js_instrument_settings.schema.json +++ b/schemas/js_instrument_settings.schema.json @@ -30,8 +30,8 @@ "title": "Properties to instrument", "description": "An array of properties to instrument on this object. If array is empty, then all properties are instrumented.", "type": ["array", "null"], - "items": - { + "items": { + "anyOf": [{ "type": "object", "properties": { "depth": { @@ -45,6 +45,11 @@ } } }, + { + "type": "string" + }] + } + , "default": [] }, "nonExistingPropertiesToInstrument": { From ce230dc02702f925e0a66ba24aa981026af1326a Mon Sep 17 00:00:00 2001 From: vringar Date: Tue, 6 Feb 2024 21:29:14 +0100 Subject: [PATCH 15/16] refactor(FirefoxService): remove patched firefox service --- openwpm/deploy_browsers/selenium_firefox.py | 31 --------------------- 1 file changed, 31 deletions(-) diff --git a/openwpm/deploy_browsers/selenium_firefox.py b/openwpm/deploy_browsers/selenium_firefox.py index 8948d2f97..42753e276 100644 --- a/openwpm/deploy_browsers/selenium_firefox.py +++ b/openwpm/deploy_browsers/selenium_firefox.py @@ -9,7 +9,6 @@ import tempfile import threading -from selenium.webdriver.firefox import service as FirefoxServiceModule from selenium.webdriver.firefox.firefox_binary import FirefoxBinary from selenium.webdriver.firefox.options import Options @@ -76,33 +75,3 @@ def run(self) -> None: if self.fifo is not None: os.unlink(self.fifo) self.fifo = None - - -class PatchedFirefoxService(FirefoxServiceModule.Service): - """Object that manages the starting and stopping of the GeckoDriver. - We need to override the constructor to be able to write to the FIFO - queue we use for log collection - """ - - def __init__( - self, - executable_path, - port=0, - service_args=None, - log_path="geckodriver.log", - env=None, - ): - super().__init__(executable_path, port, service_args, log_path, env) - if self.log_file: - os.close(self.log_file) - - if log_path: - try: - self.log_file = open(log_path, "a") - except OSError as e: - if e.errno != errno.ESPIPE: - raise - self.log_file = open(log_path, "w") - - -FirefoxServiceModule.Service = PatchedFirefoxService From d34306413c51afcf84b5c982978c067aa4135299 Mon Sep 17 00:00:00 2001 From: vringar Date: Tue, 6 Feb 2024 23:17:14 +0100 Subject: [PATCH 16/16] fix(Extension): fix eslint issues --- Extension/.eslintrc.js | 1 + Extension/src/stealth/instrument.ts | 6 +++--- Extension/src/types/js_instrument_settings.d.ts | 13 ++++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Extension/.eslintrc.js b/Extension/.eslintrc.js index 438cc1331..03fd0f616 100644 --- a/Extension/.eslintrc.js +++ b/Extension/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { ignorePatterns: [ "bundled/feature.js", "bundled/content.js", + "bundled/stealth.js", "bundled/privileged/sockets/bufferpack.js" /** This is a file we just copied in */, "build/", "dist/", diff --git a/Extension/src/stealth/instrument.ts b/Extension/src/stealth/instrument.ts index 2457ddb93..fd0f1eab9 100644 --- a/Extension/src/stealth/instrument.ts +++ b/Extension/src/stealth/instrument.ts @@ -167,7 +167,7 @@ function getOriginatingScriptContext(getCallStack = false, isCall = false) { } let traceStart = getBeginOfScriptCalls(trace); - if (traceStart == -1) { + if (traceStart === -1) { // If not included, use heuristic, 0-3 or 0-2 are OpenWPMs functions traceStart = isCall ? 3 : 4; } @@ -682,7 +682,7 @@ function instrumentGetterSetter( const getFuncType = "get"; const setFuncType = "set"; - if (descriptor.hasOwnProperty(getFuncType)) { + if (Object.prototype.hasOwnProperty.call(descriptor,getFuncType)) { instrumentedFunction = generateGetter( identifier, descriptor, @@ -697,7 +697,7 @@ function instrumentGetterSetter( propertyName, ); } - if (descriptor.hasOwnProperty(setFuncType)) { + if (Object.prototype.hasOwnProperty.call(descriptor,setFuncType)) { instrumentedFunction = generateSetter(identifier, descriptor, propertyName); injectFunction( instrumentedFunction, diff --git a/Extension/src/types/js_instrument_settings.d.ts b/Extension/src/types/js_instrument_settings.d.ts index fda16d1d5..e638042da 100644 --- a/Extension/src/types/js_instrument_settings.d.ts +++ b/Extension/src/types/js_instrument_settings.d.ts @@ -17,11 +17,14 @@ export type InstrumentedName = string; * An array of properties to instrument on this object. If array is empty, then all properties are instrumented. */ export type PropertiesToInstrument = - | { - depth?: number; - propertyNames?: string[]; - [k: string]: unknown; - }[] + | ( + | { + depth?: number; + propertyNames?: string[]; + [k: string]: unknown; + } + | string + )[] | null; /** * An array of non-existing properties to instrument on this object.