From d61720e4546f0700b8b3f626d9694c0b9d5b8601 Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:14:12 +0200 Subject: [PATCH 1/2] Initial PoC changes for testing --- injected/src/content-feature.js | 16 +++++++------ .../fingerprinting-temporary-storage.js | 1 + injected/src/features/gpc.js | 2 ++ injected/src/features/navigator-interface.js | 1 + injected/src/features/web-compat.js | 12 ++++++---- injected/src/wrapper-utils.js | 24 ++++++++++++++++++- 6 files changed, 44 insertions(+), 12 deletions(-) diff --git a/injected/src/content-feature.js b/injected/src/content-feature.js index 4745c4bcf8..6d819b7627 100644 --- a/injected/src/content-feature.js +++ b/injected/src/content-feature.js @@ -235,10 +235,12 @@ export default class ContentFeature extends ConfigFeature { /** * Define a property descriptor with debug flags. * Mainly used for defining new properties. For overriding existing properties, consider using wrapProperty(), wrapMethod() and wrapConstructor(). - * @param {any} object - object whose property we are wrapping (most commonly a prototype, e.g. globalThis.BatteryManager.prototype) - * @param {string} propertyName - * @param {import('./wrapper-utils').StrictPropertyDescriptor} descriptor - requires all descriptor options to be defined because we can't validate correctness based on TS types - */ + * @template Obj + * @template {keyof Obj} Key + * @param {Obj} object - object whose property we are wrapping (most commonly a prototype, e.g. globalThis.BatteryManager.prototype) + * @param {Key} propertyName + * @param {import('./wrapper-utils.js').StrictPropertyDescriptorGeneric} descriptor - requires all descriptor options to be defined because we can't validate correctness based on TS types + */ defineProperty(object, propertyName, descriptor) { // make sure to send a debug flag when the property is used // NOTE: properties passing data in `value` would not be caught by this @@ -247,16 +249,16 @@ export default class ContentFeature extends ConfigFeature { if (typeof descriptorProp === 'function') { const addDebugFlag = this.addDebugFlag.bind(this); const wrapper = new Proxy(descriptorProp, { - apply(_, thisArg, argumentsList) { + apply(target, thisArg, argumentsList) { addDebugFlag(); - return Reflect.apply(descriptorProp, thisArg, argumentsList); + return target.apply(thisArg, argumentsList); }, }); descriptor[k] = wrapToString(wrapper, descriptorProp); } }); - return defineProperty(object, propertyName, descriptor); + return defineProperty(object, String(propertyName), /** @type {any} */ (descriptor)); } /** diff --git a/injected/src/features/fingerprinting-temporary-storage.js b/injected/src/features/fingerprinting-temporary-storage.js index e8f57d0846..306752a310 100644 --- a/injected/src/features/fingerprinting-temporary-storage.js +++ b/injected/src/features/fingerprinting-temporary-storage.js @@ -26,6 +26,7 @@ export default class FingerprintingTemporaryStorage extends ContentFeature { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f org.call(navigator.webkitTemporaryStorage, modifiedCallback, err); }; + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'webkitTemporaryStorage', { get: () => tStorage, enumerable: true, diff --git a/injected/src/features/gpc.js b/injected/src/features/gpc.js index 2a75dc9a29..fddab8ede3 100644 --- a/injected/src/features/gpc.js +++ b/injected/src/features/gpc.js @@ -8,6 +8,7 @@ export default class GlobalPrivacyControl extends ContentFeature { if (args.globalPrivacyControlValue) { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f if (navigator.globalPrivacyControl) return; + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => true, configurable: true, @@ -18,6 +19,7 @@ export default class GlobalPrivacyControl extends ContentFeature { // this may be overwritten by the user agent or other extensions // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f if (typeof navigator.globalPrivacyControl !== 'undefined') return; + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => false, configurable: true, diff --git a/injected/src/features/navigator-interface.js b/injected/src/features/navigator-interface.js index b2a83fb1af..3e95d00b87 100644 --- a/injected/src/features/navigator-interface.js +++ b/injected/src/features/navigator-interface.js @@ -24,6 +24,7 @@ export default class NavigatorInterface extends ContentFeature { if (!args.platform || !args.platform.name) { return; } + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'duckduckgo', { value: { platform: args.platform.name, diff --git a/injected/src/features/web-compat.js b/injected/src/features/web-compat.js index 0d665fc4d2..ca8f7155ac 100644 --- a/injected/src/features/web-compat.js +++ b/injected/src/features/web-compat.js @@ -1,3 +1,4 @@ +// TypeScript is disabled for this file due to intentional DOM polyfills (e.g., Notification) that are incompatible with the DOM lib types. import ContentFeature from '../content-feature.js'; // eslint-disable-next-line no-redeclare import { URL } from '../captured-globals.js'; @@ -192,6 +193,7 @@ export class WebCompat extends ContentFeature { return; } // Expose the API + // window.Notification polyfill is intentionally incompatible with DOM lib types this.defineProperty(window, 'Notification', { value: () => { // noop @@ -200,8 +202,8 @@ export class WebCompat extends ContentFeature { configurable: true, enumerable: false, }); - - this.defineProperty(window.Notification, 'requestPermission', { + // window.Notification polyfill is intentionally incompatible with DOM lib types + this.defineProperty(/** @type {any} */ (window.Notification), 'requestPermission', { value: () => { return Promise.resolve('denied'); }, @@ -210,13 +212,13 @@ export class WebCompat extends ContentFeature { enumerable: true, }); - this.defineProperty(window.Notification, 'permission', { + this.defineProperty(/** @type {any} */ (window.Notification), 'permission', { get: () => 'denied', configurable: true, enumerable: false, }); - this.defineProperty(window.Notification, 'maxActions', { + this.defineProperty(/** @type {any} */ (window.Notification), 'maxActions', { get: () => 2, configurable: true, enumerable: true, @@ -400,6 +402,7 @@ export class WebCompat extends ContentFeature { }; // TODO: original property is an accessor descriptor this.defineProperty(Navigator.prototype, 'credentials', { + // validate this value, configurable: true, enumerable: true, @@ -416,6 +419,7 @@ export class WebCompat extends ContentFeature { if (window.safari) { return; } + // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f this.defineProperty(window, 'safari', { value: {}, writable: true, diff --git a/injected/src/wrapper-utils.js b/injected/src/wrapper-utils.js index c543edd687..1b7bdb31c5 100644 --- a/injected/src/wrapper-utils.js +++ b/injected/src/wrapper-utils.js @@ -366,16 +366,38 @@ export function shimProperty(baseObject, propertyName, implInstance, readOnly, d */ /** - * @typedef {Object} BaseStrictPropertyDescriptor + * A generic property descriptor for a property of an object, with correct `this` context for accessors. + * + * @template Obj The object type + * @template {keyof Obj} Key The property key + * @typedef {Object} StrictPropertyDescriptorGeneric * @property {boolean} configurable * @property {boolean} enumerable + * @property {boolean} [writable] + * @property {(function(this: Obj): Obj[Key]) |Obj[Key]} [value] + * @property {(function(this: Obj): Obj[Key])} [get] + * @property {(function(this: Obj, Obj[Key]): void)} [set] */ + +/** + * @typedef {Object} BaseStrictPropertyDescriptor + * @property {boolean} configurable + * @property {boolean} enumerable + * */ /** * @typedef {BaseStrictPropertyDescriptor & { value: any; writable: boolean }} StrictDataDescriptor + * */ +/** * @typedef {BaseStrictPropertyDescriptor & { get: () => any; set: (v: any) => void }} StrictAccessorDescriptor + * */ +/** * @typedef {BaseStrictPropertyDescriptor & { get: () => any }} StrictGetDescriptor + * */ +/** * @typedef {BaseStrictPropertyDescriptor & { set: (v: any) => void }} StrictSetDescriptor + * */ +/** * @typedef {StrictDataDescriptor | StrictAccessorDescriptor | StrictGetDescriptor | StrictSetDescriptor} StrictPropertyDescriptor */ From da12a40e383a9bc302a86125871723264b31311f Mon Sep 17 00:00:00 2001 From: laghee <20972610+laghee@users.noreply.github.com> Date: Thu, 7 Aug 2025 12:30:52 +0200 Subject: [PATCH 2/2] Address comments --- injected/src/content-feature.js | 33 +++++++++++++++++++++++++++-- injected/src/features/web-compat.js | 1 - 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/injected/src/content-feature.js b/injected/src/content-feature.js index 6d819b7627..57b87f9a71 100644 --- a/injected/src/content-feature.js +++ b/injected/src/content-feature.js @@ -240,7 +240,7 @@ export default class ContentFeature extends ConfigFeature { * @param {Obj} object - object whose property we are wrapping (most commonly a prototype, e.g. globalThis.BatteryManager.prototype) * @param {Key} propertyName * @param {import('./wrapper-utils.js').StrictPropertyDescriptorGeneric} descriptor - requires all descriptor options to be defined because we can't validate correctness based on TS types - */ + */ defineProperty(object, propertyName, descriptor) { // make sure to send a debug flag when the property is used // NOTE: properties passing data in `value` would not be caught by this @@ -258,7 +258,36 @@ export default class ContentFeature extends ConfigFeature { } }); - return defineProperty(object, String(propertyName), /** @type {any} */ (descriptor)); + // Build complete strict descriptor all at once so TS doesn't complain about missing properties + let strictDescriptor; + + if (descriptor.value !== undefined || descriptor.writable !== undefined) { + // Data descriptor + strictDescriptor = { + configurable: descriptor.configurable ?? false, + enumerable: descriptor.enumerable ?? false, + value: descriptor.value, + writable: descriptor.writable ?? false, + }; + } else if (descriptor.get !== undefined || descriptor.set !== undefined) { + // Accessor descriptor + strictDescriptor = { + configurable: descriptor.configurable ?? false, + enumerable: descriptor.enumerable ?? false, + get: descriptor.get ?? (() => undefined), + set: descriptor.set ?? (() => {}), + }; + } else { + // Default data descriptor + strictDescriptor = { + configurable: descriptor.configurable ?? false, + enumerable: descriptor.enumerable ?? false, + value: undefined, + writable: false, + }; + } + + return defineProperty(object, String(propertyName), strictDescriptor); } /** diff --git a/injected/src/features/web-compat.js b/injected/src/features/web-compat.js index ca8f7155ac..fbe012c1d1 100644 --- a/injected/src/features/web-compat.js +++ b/injected/src/features/web-compat.js @@ -1,4 +1,3 @@ -// TypeScript is disabled for this file due to intentional DOM polyfills (e.g., Notification) that are incompatible with the DOM lib types. import ContentFeature from '../content-feature.js'; // eslint-disable-next-line no-redeclare import { URL } from '../captured-globals.js';