Device enumeration getCapabilities fix - #2688
Conversation
[Beta] Generated file diffTime updated: Tue, 12 May 2026 14:01:59 GMT Android
File has changed AppleFile has changed IntegrationFile has changed WindowsFile has changed |
|
This PR requires a manual review and approval from a member of one of the following teams:
|
Build Branch
Static preview entry points
QR codes (mobile preview)
Integration commandsnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", branch: "pr-releases/jkt/auto/device-enumeration-getcapabilities-fix-5de6")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/jkt/auto/device-enumeration-getcapabilities-fix-5de6
git -C submodules/content-scope-scripts checkout origin/pr-releases/jkt/auto/device-enumeration-getcapabilities-fix-5de6Pin to exact commitnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "1315a26e9811b8b4b08de4068713d08cdb0770f8")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/jkt/auto/device-enumeration-getcapabilities-fix-5de6
git -C submodules/content-scope-scripts checkout 1315a26e9811b8b4b08de4068713d08cdb0770f8 |
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1223-1236- warning: The new syntheticgetCapabilitiesfixes the illegal-invocation path, but it is detectably different from nativeInputDeviceInfo.prototype.getCapabilities. In Chromium the native method is inherited from the prototype, has.name === "getCapabilities",.length === 0, descriptor flags{ writable: true, configurable: true, enumerable: true }, and nativetoString()output. The PR installs an own, unmasked JavaScript function with different descriptor flags (writable: false,enumerable: false), so scripts checkinghasOwnProperty, descriptors,for...in, or function source can detect the override and branch differently.injected/playwright.config.js:331-336- info: The regression test is added only tochrome-mv3. The implementation is shared with Windows and the PR description calls out Windows breakage, so this leaves a platform-specific coverage gap, but not a runtime compatibility issue by itself.Security Assessment
injected/src/features/web-compat.js:1223-1236- warning: This path now calls page-reachableObject.definePropertyatenumerateDevices()call time and readsdeviceInfo.getCapabilitiesfrom the live prototype chain. In hostile page contexts, a page can monkey-patch these globals/prototypes before invokingenumerateDevices(), causing the shim to throw, alter the returned method, or observe the protection path. Prefer capturedobjectDefineProperty/wrapper utilities and a captured nativeInputDeviceInfo.prototype.getCapabilitiesreference where possible.- No messaging,
nativeData, origin-validation,postMessage, iframe, or new network/CSS-injection issues found in this diff.Risk Level
High Risk - The change adds a new method shim/
definePropertypath to an injected browser API object returned to page scripts; it is config-gated, but still changes observable API surface across platforms wherewebCompat.enumerateDevicesis enabled.Recommendations
- Preserve native method fidelity: use a named
function getCapabilities() { return {}; }, mask it withwrapToString()against the native method, and align descriptor flags with the native method. If feasible, use a shadow prototype sodevice.hasOwnProperty('getCapabilities')remains false whiledevice instanceof InputDeviceInforemains true.- Replace the raw
Object.definePropertywith a captured define-property helper, and avoid looking upgetCapabilitiesfrom page-mutable prototypes at call time.- Extend the regression test to assert
.name,.length,toString(), descriptor/enumerability behavior, and add Windows-project coverage or document why the extension harness is the intended proxy.Validation run:
npm --workspace injected run test-int -- --project=chrome-mv3 integration-test/device-enumeration.spec.js --reporter=listpassed (5 tests).Sent by Cursor Automation: Web compat and sec
Use the existing wrapToString helper so the synthetic InputDeviceInfo.getCapabilities shim returns a native-looking toString() (and toString.toString()), and align descriptor flags (writable/configurable/enumerable: true) with native prototype methods. Extends the device-enumeration integration test to assert the masked toString output. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1223-1235- info: The updated syntheticInputDeviceInfo#getCapabilities()shim preserves the expected return contract ({}), uses a named function, maskstoString()/toString.toString()withwrapToString(), and aligns writable/configurable/enumerable flags with the native prototype method. The method is still an own property rather than inherited, but these synthetic devices already expose own data properties, so I do not see this as a blocking compatibility regression.injected/playwright.config.js:79-90- info: The regression suite is now exercised in thechrome-mv3project. This covers the extension harness, but the runtime code is shared with native bundles, so Windows/Android/Apple coverage remains a test-depth gap rather than a runtime issue.Security Assessment
injected/src/features/web-compat.js:1223- warning:typeof deviceInfo.getCapabilitiesreads through the liveInputDeviceInfo.prototypechain atenumerateDevices()call time. A hostile page can replace that prototype slot with a getter or non-function before callingenumerateDevices(), causing the compat path to skip/throw and fall back to native enumeration. Prefer checking a captured/native descriptor or method reference instead of touching the synthetic object's page-mutable prototype chain.- No
nativeData, message bridge, origin-validation,postMessage, iframe, network, or CSS-injection issues found in this diff.Risk Level
High Risk - The PR changes the observable surface of an injected browser API object returned to page scripts, albeit behind the existing
webCompat.enumerateDevicesconfig gate.Recommendations
- Replace the
deviceInfo.getCapabilitiescapability check with a captured native method/descriptor check, e.g. a module-load reference or capturedgetOwnPropertyDescriptor(InputDeviceInfo.prototype, 'getCapabilities'), so page prototype tampering cannot alter the shim decision.- If this fix is intended for native app breakage, add at least one native-bundle integration run or document why
chrome-mv3is the intended proxy.Validation:
npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listpassed (5 tests);git diff --checkpassed.Sent by Cursor Automation: Web compat and sec
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/playwright.config.js, line 347 - error: Addingintegration-test/device-enumeration.spec.jsto thechrome-mv3project currently makes the targeted suite fail.npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listfailed 1/5 because the disabled-feature test readwindow.resultsasundefined. The page sets results asynchronously fromrenderResults(), so the spec needs to wait forresults-ready/window.resultsbefore asserting.injected/src/features/web-compat.js, lines 1175-1176 - warning: The intermediate prototype fixes the own-method detectability, but it changes prototype identity for synthetic input devices (Object.getPrototypeOf(device) !== InputDeviceInfo.prototype). That is an observable API-surface difference for scripts that inspect__proto__chains. If this tradeoff is intentional, add a regression assertion documenting it.Security Assessment
injected/src/features/web-compat.js, line 1176 - warning:typeof syntheticInputDeviceInfoPrototype.getCapabilitiesstill reads through the live page-mutableInputDeviceInfo.prototypechain atenumerateDevices()call time. A hostile page can replace that slot with a getter that throws or returns a non-function, forcing the catch path to fall back to native enumeration. Prefer checking a captured/native descriptor or method reference instead of touching the synthetic object's prototype chain.- No
nativeData, message bridge, origin-validation,postMessage, iframe, network, or CSS-injection issues found in this diff.Risk Level
High Risk - The PR changes the observable surface of an injected browser API object returned to page scripts and enables a new integration suite for that path.
Recommendations
- Fix the newly enabled integration test by waiting for the test page to publish results before reading
window.results; rerun thechrome-mv3device-enumeration target.- Avoid the live prototype-chain read for
getCapabilities; use a captured descriptor/method check.- Add explicit tests for the accepted prototype-chain shape if the shadow prototype is the chosen compatibility tradeoff.
Sent by Cursor Automation: Web compat and sec
Adds a per-site config switch under webCompat.enumerateDevices so we can react to breakage in the wild without redeploying code:
- 'syntheticPrototype' (default, unchanged): intermediate prototype with own getCapabilities. Hides hasOwnProperty('getCapabilities'); one-level prototype-chain depth difference.
- 'instanceOwn': preserve InputDeviceInfo.prototype as the direct prototype; place an own masked getCapabilities on the instance. Restores Object.getPrototypeOf(d) === InputDeviceInfo.prototype.
- 'disabled': no shim; native brand check throws TypeError 'Illegal invocation'.
Also fixes the previously flaky disabled/enabled tests by waiting for the renderResults 'results-ready' event before reading window.results, and adds coverage for the two new modes.
Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1173-1322- info: The latest revision adds explicitgetCapabilitiesShimmodes. The defaultsyntheticPrototypepath preserves the non-throwinggetCapabilities()contract for synthetic input devices, maskstoString()/toString.toString(), and keeps a per-site escape hatch for the direct-prototype and disabled semantics.injected/playwright.config.js:83andinjected/integration-test/device-enumeration.spec.js:8-351- info: The previously failingchrome-mv3device-enumeration target is now covered with the asyncresults-readywait and mode-specific assertions.Security Assessment
- No new actionable security findings in this revision. I did not find changes to message bridge trust boundaries, origin validation,
nativeData,postMessage, iframe access, network fetches, or CSS injection.- The live
InputDeviceInfo.prototype/getCapabilitieslookup is the same residual hardening point discussed earlier; the new config switch documents and contains the compatibility trade-off, so I am not re-raising it as a duplicate blocker.Risk Level
High Risk - This still changes the observable surface of an injected browser API object returned to page scripts, but it is config-gated and now has targeted integration coverage for the supported modes.
Recommendations
- No blocking changes from this pass.
- If this path is hardened further, prefer a captured/native descriptor or method reference for the
getCapabilitiescapability decision so page prototype tampering cannot influence shim selection.Validation:
npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listpassed (7 tests), andgit diff --check origin/main...origin/jkt/auto/device-enumeration-getcapabilities-fix-5de6passed.Sent by Cursor Automation: Web compat and sec
If a site needs the unaltered native semantics, the entire enumerateDevices feature can already be disabled, in which case no synthetic devices are produced and the native getCapabilities works normally. The 'disabled' shim mode kept producing synthetic devices but reintroduced the illegal-invocation throw, which is just the original bug. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
Both shim modes (syntheticPrototype, instanceOwn) build the same masked no-op descriptor for getCapabilities and call this.defineProperty with the same flags. Pull that into a single helper to keep the createMediaDeviceInfo branches readable. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1171-1326- info: The current revision keeps thegetCapabilities()fix behind the existingwebCompat.enumerateDevicessetting. The defaultsyntheticPrototypemode avoids the native brand-check throw for synthetic input devices, preservesinstanceof InputDeviceInfo, returns{}, and masks bothtoString()andtoString.toString()in the covered path. TheinstanceOwnmode remains available for sites that need directInputDeviceInfo.prototypeidentity, with the expected own-property trade-off.injected/integration-test/device-enumeration.spec.js:13-305andinjected/playwright.config.js:406-417- info: The Chrome MV3 regression coverage now waits for async page results and covers the default shim plusinstanceOwnbehavior. I do not see a runtime web-compat blocker in the current test/config changes.Security Assessment
- No new actionable security findings in this revision. I did not find changes to message bridge trust boundaries, origin validation,
nativeData,postMessage, iframe access, network fetches, or CSS injection.- Residual hardening note, not a blocker: the shim decision still reads from the live
InputDeviceInfo.prototypechain when checkingtypeof ...getCapabilities; a captured/native descriptor would be stronger against hostile prototype tampering, but this is the same non-blocking point raised in earlier passes.Risk Level
High Risk - The PR changes the observable surface of an injected browser API object returned to page scripts, though the behavior is config-gated and covered by targeted integration tests.
Recommendations
- No blocking changes from this pass.
- If hardening continues, prefer a captured
InputDeviceInfo.prototype.getCapabilitiesdescriptor or method reference for the capability decision.Validation:
npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listpassed (6 tests), andgit diff --check c0029efd3832e24977d83ca656e65d3042b3a2d2...39c2b5a96c258ad1178b1268f175ac37c37618aepassed.Sent by Cursor Automation: Web compat and sec
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1160-1322- info: The current revision keeps the syntheticInputDeviceInfo#getCapabilities()path masked withwrapToString()includingtoString.toString(), and preserves native-like method descriptor flags. The defaultsyntheticPrototypemode intentionally trades direct prototype identity for hiding the shim from instancehasOwnProperty()checks;instanceOwnremains available for sites that requireObject.getPrototypeOf(device) === InputDeviceInfo.prototype.injected/src/features/web-compat.js:1290-1291- info: UnknowngetCapabilitiesShimvalues now fall back tosyntheticPrototype; there is no longer a separatedisabledmode in this revision. That is acceptable from this pass because the wholeenumerateDevicessubfeature remains remote-config gated, but it does mean rollback is at the subfeature level rather than the individual shim behavior.injected/playwright.config.js:83andinjected/integration-test/device-enumeration.spec.js:8-322- info: Thechrome-mv3coverage now exercises the async result wait, native illegal-invocation regression case, default synthetic-prototype shim, andinstanceOwncompatibility mode.Security Assessment
- No new actionable security findings in this synchronized revision. I did not find changes to message bridge trust boundaries, origin validation,
nativeData,postMessage, iframe access, network fetches, or CSS injection.- The live
InputDeviceInfo.prototype/getCapabilitieslookup remains the same residual hardening point discussed earlier; I am not re-raising it as a duplicate blocker.Risk Level
High Risk - This changes the observable surface of an injected browser API object returned to page scripts, but it is config-gated and has targeted integration coverage for the supported modes.
Recommendations
- No blocking changes from this pass.
- If this path is hardened further, prefer a captured/native descriptor or method reference for the
getCapabilitiescapability decision so page prototype tampering cannot influence shim selection.Validation:
npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listpassed (6 tests), andgit diff --check c0029efd3832e24977d83ca656e65d3042b3a2d2...FETCH_HEADpassed.Sent by Cursor Automation: Web compat and sec
…viceInfo prototypes Replaces the single-method getCapabilities shim with a generic prototype-walker that defines a masked no-op for every function-valued own property on MediaDeviceInfo.prototype and InputDeviceInfo.prototype. Explicit instance-level definitions (deviceId, kind, label, groupId, toJSON) win, so toJSON semantics are preserved. Renames the per-site setting from 'getCapabilitiesShim' to 'shimMode' since it now covers every method in the space, not just getCapabilities. Adds a regression test that plants a synthetic future method on InputDeviceInfo.prototype and verifies it gets auto-shimmed with a native-looking name/toString, while toJSON keeps its explicit own behavior. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1174-1196- error:defineSyntheticMethodShims()scans the liveInputDeviceInfo.prototype/MediaDeviceInfo.prototypechain atenumerateDevices()call time and installs DDG no-op shims for every function-valued property it finds. That shadows page/polyfill/analytics methods added or wrapped beforeenumerateDevices()runs; realMediaDeviceInfoobjects would inherit the page's method, but synthetic devices now return{}instead. The new test atinjected/integration-test/device-enumeration.spec.js:307-365codifies this by addingfuturePrototypeMethodon the page prototype and expecting the DDG shim to replace it, which is a third-party script compatibility regression.Security Assessment
injected/src/features/web-compat.js:1175-1183- warning: the new helper re-reads page-reachable globals (Set,Object.getOwnPropertyNames,Object.getOwnPropertyDescriptor,Object.prototype.hasOwnProperty) when the page callsenumerateDevices(). A hostile page can monkey-patch these to suppress shim discovery, force the catch/fallback path, or cause arbitrary page-added methods to be treated as protected native methods. Prefer captured references fromcaptured-globals.jsand add a capturedgetOwnPropertyNamesexport if this helper remains generic.- No
nativeData, message bridge, origin-validation,postMessage, iframe, network, or CSS-injection issues found in this diff.Risk Level
High Risk - This changes the observable API surface of injected
MediaDeviceInfo/InputDeviceInfoobjects returned to page scripts, including prototype-chain behavior, although it remains behind the existingwebCompat.enumerateDevicesconfig gate.Recommendations
- Restrict method shimming to native descriptors captured before page script mutation, or hard-code the currently needed native method (
getCapabilities) rather than scanning live page-mutable prototypes.- If a prototype method has been replaced by page code, preserve that page-visible method semantics instead of shadowing it with a DDG
{}shim.- Use captured globals for the helper (
Set, descriptor lookup,hasOwnProperty, and a new capturedgetOwnPropertyNames) and add a regression test where a page-defined prototype method still runs on synthetic devices.Validation:
npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listpassed (7 tests), andgit diff --check c0029efd3832e24977d83ca656e65d3042b3a2d2...69acd373711ea8cf470549c2c0aff88c77b38004passed.Sent by Cursor Automation: Web compat and sec
…ing name Per review: the generic for-loop over MediaDeviceInfo/InputDeviceInfo prototypes is overkill for the only known problem method (getCapabilities). Revert to the single defineSyntheticGetCapabilities helper, but keep the per-site setting named 'shimMode' (not 'getCapabilitiesShim') so we can extend it to other shims later without another config rename. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
injected/src/features/web-compat.js:1166-1245- info: The current revision no longer scans liveMediaDeviceInfo/InputDeviceInfoprototypes for arbitrary function properties. It is back to a singlegetCapabilitiesshim, masked withwrapToString()includingtoString.toString(), and defined viathis.defineProperty()so the captured define-property path is used.injected/src/features/web-compat.js:1201-1224- info: The defaultsyntheticPrototypemode intentionally hidesgetCapabilitiesfrom instancehasOwnProperty()checks while preservinginstanceof InputDeviceInfo;instanceOwnremains available for sites that need directObject.getPrototypeOf(device) === InputDeviceInfo.prototypesemantics. This is still observable API-surface shaping, but the trade-off is config-selectable under the existingwebCompat.enumerateDevicesgate.injected/integration-test/device-enumeration.spec.jsandinjected/playwright.config.js- info: The Chrome MV3 regression coverage now waits for async page results and exercises the native illegal-invocation case, default synthetic-prototype shim, maskedtoString(), delete behavior, andshimMode=instanceOwn.Security Assessment
- No new actionable security findings in this revision. I did not find changes to message bridge trust boundaries, origin validation,
nativeData,postMessage, iframe access, network fetches, CSS injection, or captured-globals-sensitive messaging paths.- Residual hardening note, not a blocker:
defineSyntheticGetCapabilities()still decides whether to install the shim by readingtarget.getCapabilitiesthrough the live page-visible prototype chain. A captured native descriptor would be stronger against hostile prototype tampering, but this is the same non-blocking point from earlier revisions.Risk Level
High Risk - The PR changes the observable surface of synthetic
MediaDeviceInfo/InputDeviceInfoobjects returned to page scripts, though it is config-gated and now narrowly scoped to the knowngetCapabilitiescompatibility issue.Recommendations
- No blocking changes from this pass.
- If this path is hardened further, prefer a captured/native
InputDeviceInfo.prototype.getCapabilitiesdescriptor or method reference for the capability decision.Validation:
npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=listpassed (6 tests), andgit diff --check c0029efd3832e24977d83ca656e65d3042b3a2d2...8cd98eee9ca761f7ff3bd2e7610a7a2872f5faadpassed.Sent by Cursor Automation: Web compat and sec
There was a problem hiding this comment.
Web Compatibility Assessment
injected/src/features/web-compat.js:1167-1322- info: The runtime change remains narrowly scoped to syntheticInputDeviceInfo#getCapabilities()for the existingwebCompat.enumerateDevicespath. The shim is named,wrapToString()-masked includingtoString.toString(), returns{}, and keeps thesyntheticPrototype/instanceOwntrade-off configurable.injected/integration-test/helpers/harness.js:25-34- info: The latest revision only changes the Chrome extension test harness to resolve the extension directory to an absolute path before passing it to Chromium. I do not see an injected-runtime compatibility issue from this test-only change.injected/integration-test/device-enumeration.spec.js:8-305andinjected/playwright.config.js:521-532- info: Chrome MV3 coverage now exercises the async result wait, native illegal-invocation regression case, default shim behavior, masking, delete behavior, andshimMode=instanceOwn.
Security Assessment
- No new actionable security findings in this revision. I did not find changes to message bridge trust boundaries, origin validation,
nativeData,postMessage, iframe access, network fetches, CSS injection, or captured-globals-sensitive messaging paths. - Residual hardening note, not a blocker:
defineSyntheticGetCapabilities()still decides whether to install the shim by readingtarget.getCapabilitiesthrough the live page-visible prototype chain. A captured native descriptor would be stronger against hostile prototype tampering, but this is unchanged from the prior assessment.
Risk Level
High Risk - The PR changes the observable surface of synthetic MediaDeviceInfo / InputDeviceInfo objects returned to page scripts, though it is config-gated and now has targeted integration coverage.
Recommendations
- No blocking changes from this pass.
- If hardening continues, prefer a captured/native
InputDeviceInfo.prototype.getCapabilitiesdescriptor or method reference for the capability decision.
Validation: npm run test-int -- integration-test/device-enumeration.spec.js --project=chrome-mv3 --reporter=list passed (6 tests), and git diff --check c0029efd3832e24977d83ca656e65d3042b3a2d2...abbaf83fa0ee010da4f7f02e43d04075847797b7 passed.
Sent by Cursor Automation: Web compat and sec
There was a problem hiding this comment.
LGTM overall (didn't test directly with windows in the interest of time, but could if needed). We could reuse a shared prototype here (create one synthetic per web-compat instance instead of per device), but doesn't seem vital (also maybe there's some potential side-effect to doing that).
Yeah, I don't think we need to do that. The reason to share prototypes is to prevent prototype pollution. There wasn't really a risk of this here at all. |


Asana Task/Github Issue: https://app.asana.com/1/137249556945/task/1214684048521837?focus=true
Description
Adds getCapabilities to fix issues with sites on Windows. See: duckduckgo/privacy-configuration#5136
Testing Steps
Checklist
Please tick all that apply:
Note
Medium Risk
Changes the behavior of
navigator.mediaDevices.enumerateDevices()results by overridingInputDeviceInfo.getCapabilities()for synthetic devices, which could impact site compatibility if the shim diverges from native behavior. Risk is mitigated by being scoped to thewebCompatenumerateDevicessetting and covered by new integration tests.Overview
Ensures synthetic
InputDeviceInfoobjects returned by thewebCompatenumerateDevicesshim won’t throw when sites callgetCapabilities()by defining a safegetCapabilities()implementation that returns{}for input devices.Adds Playwright coverage that demonstrates native
getCapabilities()throws on legacy syntheticInputDeviceInfoobjects, validates the shimmed behavior for enumerated input devices, and includesdevice-enumeration.spec.jsin thechrome-mv3project test set.Reviewed by Cursor Bugbot for commit fea09d0. Bugbot is set up for automated code reviews on this repo. Configure here.