Capture message handler refs at construction on WebKit#2729
Conversation
The WebkitMessagingTransport previously only captured handler references at construction time on legacy WebKit (macOS Catalina, < 11). On modern WebKit it instead re-read window.webkit.messageHandlers[handler] freshly on every send. That meant the transport silently broke if site-level privacy hardening replaced or removed window.webkit.messageHandlers after init. Concretely, the apiManipulation feature can be used to install a prototype-side getter that nullifies messageHandlers for site JS (to reduce window.webkit fingerprinting surface on external sites). Without this change, any page-world C-S-S feature that calls notify/request/subscribe after the nullify would synchronously throw inside wkSend reading undefined[handler] — and silently degrade because the surrounding polyfills (e.g. webCompat Notification) catch the throw and return defaults. After this change: - captureWebkitHandlers runs unconditionally in the constructor on both legacy and modern WebKit. - On legacy, the original postMessage is still deleted from the host handler (preserving the existing security posture for macOS < 11). - On modern, the original postMessage is left in place so any other code reading through window.webkit.messageHandlers continues to see the host binding's normal shape. - wkSend uses the captured reference on both paths. Adds unit tests in injected/unit-test/messaging-webkit.spec.js covering: - modern capture + routing through captured ref - modern leaves original postMessage in place - modern transport survives wholesale replacement of window.webkit.messageHandlers - modern throws MissingHandler when a handler was never registered - legacy still deletes original postMessage - legacy still adds the secure messaging envelope (secret)
[Beta] Generated file diffTime updated: Thu, 28 May 2026 18:11:24 GMT Android
File has changed Apple
File has changed Chrome-mv3File has changed FirefoxFile has changed Integration
File has changed Windows
File has changed |
|
This PR requires a manual review and approval from a member of one of the following teams:
|
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
No direct API-surface or DOM-compat regressions found in the changed lines. The transport still preserves the modern WebKit
postMessagereturn contract and leaves the host handler property intact.Security Assessment
Warning:
messaging/lib/webkit.jsnow makescapturedWebkitHandlersauthoritative on modern WebKit. The cache is a normal object, so lookup should be own-property-only to avoid hostileObject.prototypepollution changing a missing handler into a callable page-controlled function. See inline comment.Risk Level
High Risk: this touches the WebKit messaging transport used by injected Apple surfaces, but the behavioral change is narrowly scoped and covered by targeted unit tests.
Recommendations
Use a null-prototype cache or captured
hasOwnProperty.call(...)before invoking a captured handler, and add a regression test withObject.prototype[context]polluted.Validation:
npm run test-unit -- --filter=WebkitMessagingTransportpassed in a temporary PR worktree (6 specs).Sent by Cursor Automation: Web compat and sec
- Use Object.create(null) for capturedWebkitHandlers so a hostile page cannot supply a callable via Object.prototype pollution if a capture miss ever occurs. Add a unit-test that pollutes Object.prototype with a callable for an unregistered handler name, then asserts wkSend still throws MissingHandler rather than invoking the polluted function with outgoing native payload (per Bugbot review feedback). - Slim the captureWebkitHandlers docstring to drop the legacy/modern split (per Jonathan's review feedback) while preserving the contract.
There was a problem hiding this comment.
Web Compatibility Assessment
No new web-compat regression found in the changed lines. The modern WebKit path still preserves the host postMessage binding on window.webkit.messageHandlers, and targeted unit coverage now exercises the post-init namespace replacement behavior.
Security Assessment
Warning: messaging/lib/webkit.js has one remaining global-capture issue. See inline comment.
Risk Level
High Risk: this changes the WebKit messaging transport used by Apple injected and special-page surfaces, but the behavior is narrowly scoped and covered by focused unit tests.
Recommendations
Use a tamper-resistant null-prototype cache construction, either a literal such as { __proto__: null } or a captured objectCreate exported from captured-globals.js, and add a regression test where Object.create is replaced before new WebkitMessagingTransport(...).
Validation: npm run test-unit --workspace=injected -- --filter=WebkitMessagingTransport passed (7 specs).
Sent by Cursor Automation: Web compat and sec
Adds @ts-expect-error comments on the test-only ad-hoc Object.prototype mutation and its cleanup, since the property is intentionally not part of the Object type.
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/kmC/webkit-transport-capture-at-init-7465")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/kmC/webkit-transport-capture-at-init-7465
git -C submodules/content-scope-scripts checkout origin/pr-releases/kmC/webkit-transport-capture-at-init-7465Pin to exact commitnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "85564f57b4c94e7534b2906ba8e4f53b5fc732ca")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/kmC/webkit-transport-capture-at-init-7465
git -C submodules/content-scope-scripts checkout 85564f57b4c94e7534b2906ba8e4f53b5fc732ca |
The eslint-disable-next-line was shifted out of position by inserting the @ts-expect-error comment between it and the offending assignment, so the linter was suppressing the wrong line. Reorder so eslint-disable sits directly above the Object.prototype mutation.
All call sites pass hasModernWebkitAPI: true and have done so for a considerable amount of time, so the legacy macOS < 11 secure-messaging flow is genuinely dead code. Per review discussion on PR #2729, removing it now. This change is JS-side only. apple-browsers Swift code may continue sending hasModernWebkitAPI and secret in the serialized config JSON; the JS WebkitMessagingConfig constructor simply ignores extra fields, so no cross-repo coordination is required. apple-browsers cleanup of the now-unused fields (UserScriptMessaging.swift, UserScriptMessagingSchema.swift, AutofillUserScript+SourceProvider.swift availability check, the bundled youtube-inject.bundle.js) can land independently as a follow-up. Removed from messaging/lib/webkit.js: - The hasModernWebkitAPI and secret params/fields on WebkitMessagingConfig - The if (!hasModernWebkitAPI) branches in wkSend, wkSendAndWait, captureWebkitHandlers - Helper methods only used by the legacy crypto flow: generateRandomMethod, randomString, createRandMethodName, algoObj, createRandKey, createRandIv, decryptResponse - The SecureMessagingParams export (only used internally by the legacy path, not re-exported from messaging/index.js) - Now-unused captured-global imports: _TextDecoder, _Uint8Array, _Uint32Array, Arrayfrom, _Promise, getRandomValues, generateKey, exportKey, importKey, decrypt - The second @example block in the WebkitMessagingTransport docstring describing the macOS 10 encrypted-callback flow Updated callers to drop the now-unused params: - injected/entry-points/apple.js - special-pages/shared/create-special-page-messaging.js - messaging/lib/examples/webkit.example.js Updated unit tests to match: - Removed the legacy-WebKit describe block (delete-original-postMessage and secret-envelope tests, both for code paths that no longer exist) - Removed unused params from the modern test setups webkit.js: 422 -> 188 lines.
There was a problem hiding this comment.
Web Compatibility Assessment
messaging/lib/webkit.js:36-46— warning: the capture still happens whenWebkitMessagingTransportis constructed, but injectedContentFeature.messagingis lazy. Ifwindow.webkit.messageHandlersis hidden/replaced before the first feature accesses messaging,captureWebkitHandlers()still throwsMissingHandler; the current test only covers replacement after transport construction.
Security Assessment
messaging/lib/webkit.js:122-123— error: modern WebKit now uses the legacy capture path, which calls the page-mutableFunction.prototype.bindat transport construction time. A hostile page can replacebindbefore lazy construction and makecapturedWebkitHandlers[context]an attacker-controlled function that receives outgoing native-bound messages.messaging/lib/webkit.js:30— warning: the existing open thread about uncapturedObject.create(null)still reproduces; I did not duplicate that inline comment.
Risk Level
High Risk: this changes the WebKit messaging transport used by Apple injected and special-page surfaces, and the new modern capture path has hostile-page prototype-tampering exposure.
Recommendations
- Avoid
.bind()on the host method after page code can run; store{ handler, postMessage }and call with capturedReflectApply(postMessage, handler, [data]), or import a capturedFunction.prototype.bindequivalent. - Move handler capture to the earliest Apple entrypoint/config path if the goal is resilience after web-compat hiding, or eagerly instantiate the transport before any messageHandlers hardening can run.
- Add regressions for tampered
Function.prototype.bind, tamperedObject.create, andmessageHandlersremoval beforenew Messaging(...).
Validation: focused WebKit specs pass (npm run test-unit --workspace=injected -- --filter=WebkitMessagingTransport, 5 specs), and an ad-hoc tampered-bind repro diverts wkSend() to attacker code.
Sent by Cursor Automation: Web compat and sec
Per Bugbot review feedback on PR #2729: even with Object.create(null) as the cache, the construction goes through globalThis.Object.create — which page JS can replace before transport construction runs, because Messaging is lazy on ContentFeature.messaging. Same concern applies to Function.prototype.bind used at capture time. - Switch capturedWebkitHandlers initializer from Object.create(null) to the { __proto__: null } literal, which is a syntactic construct rather than a method dispatch and so cannot be tampered with by page JS. - Stop calling .bind() at capture time. Store the handler object and the raw postMessage function as a { handler, postMessage } pair, and dispatch in wkSend via the captured ReflectApply (already imported from captured-globals.js, which captures Reflect.apply.bind(Reflect) at module-load time before any page JS can run). Adds two regression tests: - 'cache is not derived from a tampered Object.create' replaces Object.create with a tampered factory before the transport is constructed and asserts that the cache still rejects an unknown handler name with MissingHandler (rather than resolving a property injected by the tampered factory). - 'stores handler and postMessage as a separate pair' structurally asserts the capture stores raw references rather than a bound function, proving Function.prototype.bind is not on the capture or send path. (Direct Function.prototype.bind tampering in a Jasmine test breaks Jasmine itself because its spy machinery uses bind, so the structural assertion is the safe equivalent.) - 'dispatches through the captured handler with the correct `this`' asserts the send path preserves host-binding semantics via ReflectApply rather than a bare unbound call.


Asana Task/Github Issue: https://app.asana.com/1/137249556945/task/1212684734587032?focus=true
Description
The WebkitMessagingTransport previously only captured handler references at construction time on legacy WebKit (macOS Catalina, < 11). On modern WebKit it re-read
window.webkit.messageHandlers[handler]on every send. With this change, theapiManipulationfeature can be used to install a prototype-side getter that nullifiesmessageHandlersfor site JS. Without this change, any page-world C-S-S feature that calls notify/request/subscribe after the nullify would synchronously throw inside wkSend reading undefined[handler] and silently degrade because the surrounding polyfills (e.g., webCompat Notification) catch the throw and return defaults.After this change:
Adds unit tests covering:
Testing Steps
Checklist
Please tick all that apply:
Note
Medium Risk
Changes the native bridge send path for all Apple WebKit injects; behavior is intentional for privacy hardening but any missed handler capture or platform still on legacy WebKit would break messaging.
Overview
WebKit messaging now captures
messageHandlersat transport construction and sends only through that cache, so content-scope features keep talking to native after site hardening (e.g.apiManipulationnullifyingwindow.webkit.messageHandlers).wkSenduses a null-prototype handler cache and capturedReflectApplyon stored{ handler, postMessage }pairs (no per-send lookup, no.bind, hostpostMessageleft onwindow.webkit). Legacy Catalina encrypted/async reply path,SecureMessagingParams, andhasModernWebkitAPI/secretonWebkitMessagingConfigare removed; Apple entry points and special-pages wiring only pass handler names.New unit tests cover capture/routing, survival after
messageHandlersreplacement, missing handlers, prototype pollution, tamperedObject.create, and correctthison dispatch.Reviewed by Cursor Bugbot for commit 8f93c79. Bugbot is set up for automated code reviews on this repo. Configure here.