Skip to content

Added autofillPasskeys feature that intercepts navigator.credentials.get() on Windows - #2571

Merged
BalintFarkas merged 41 commits into
mainfrom
balint/feature/passkey-autofill
Jun 2, 2026
Merged

Added autofillPasskeys feature that intercepts navigator.credentials.get() on Windows#2571
BalintFarkas merged 41 commits into
mainfrom
balint/feature/passkey-autofill

Conversation

@BalintFarkas

@BalintFarkas BalintFarkas commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Asana Task/Github Issue: https://app.asana.com/1/137249556945/project/72649045549333/task/1213359471378866?focus=true

Description

Add a new autofillPasskeys feature that intercepts navigator.credentials.get() calls with mediation: 'conditional' on Windows, enabling passkey autofill via Windows Hello.
When a site requests conditional mediation, the feature notifies the host ({ Feature: 'Autofill', Name: 'getPasskeys', Data: { rpId } }), which populates the autofill dropdown with OS-managed passkeys. When the user picks one, the host replies with passkeySelected and the feature calls the real navigator.credentials.get() with the selected credential, triggering Windows Hello. Non-conditional calls pass through unmodified.
Key design decisions:

  • Uses this.wrapMethod() on CredentialsContainer.prototype for API wrapping (automatic toString spoofing and debug flag integration)
  • Domain exclusions handled by standard C-S-S exceptions in privacy-config
  • If the user has disabled passkey autofill, the intercepted promise stays pending (no native fallback)
  • Windows-only: registered in platformSupport.windows, not bundled into other platforms

Testing Steps

  • Run npm run build and verify autofillPasskeys class appears in build/windows/contentScope.js
  • Verify class AutofillPasskeys does NOT appear in non-Windows bundles (e.g. build/android/contentScope.js)
  • On a Windows DuckDuckGo browser build with passkey support, visit a site using conditional mediation (e.g. github.com login) and confirm passkey autofill works
  • On an excluded domain (via privacy-config exceptions), verify the feature does not initialize

Checklist

  • I have tested this change locally
  • I have tested this change locally in all supported browsers
  • This change will be visible to users
  • I have added automated tests that cover this change
  • I have ensured the change is gated by config
  • This change was covered by a ship review
  • This change was covered by a tech design
  • Any dependent config has been merged

Note

High Risk
Intercepts WebAuthn credential retrieval on Windows, which is authentication-critical; incorrect handling could break logins or alter conditional passkey behavior even though non-conditional paths pass through.

Overview
Adds autofillPasskeys on Windows so sites using navigator.credentials.get() with mediation: 'conditional' can register pending requests with native autofill and complete sign-in after the user picks a passkey.

The feature wraps CredentialsContainer.prototype.get (singleton only) via wrapMethod. Secure-context conditional publicKey calls notify the host with registerPasskeyRequest (rpId, requestId); passkeySelected supplies a base64 credential ID, which is decoded and passed to the real get() with allowCredentials. Other calls, missing APIs, or failed randomUUID pass through unchanged. Pending requests honor AbortSignal, supersede each other, and mirror native promise semantics for throwing option getters.

platformSupport.windows enables the feature; integration tests exclude it alongside other host-dependent features. captured-globals gains atob, DOMException, and charCodeAt for decoding; JSON schemas document the notify/subscribe payloads; strict-core coverage includes the new module.

Reviewed by Cursor Bugbot for commit 726e5fd. Bugbot is set up for automated code reviews on this repo. Configure here.

@BalintFarkas
BalintFarkas requested review from a team as code owners March 24, 2026 17:28
@BalintFarkas
BalintFarkas requested a review from quanganhdo March 24, 2026 17:28
@github-actions
github-actions Bot requested a review from dbajpeyi March 24, 2026 17:28
@github-actions github-actions Bot added the semver-minor New feature — triggers minor version bump label Mar 24, 2026
@github-actions

github-actions Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

[Beta] Generated file diff

Time updated: Tue, 02 Jun 2026 12:03:29 GMT

Android
    - android/adsjsContentScope.js
  • android/autofillImport.js
  • android/brokerProtection.js
  • android/contentScope.js
  • android/duckAiChatHistory.js
  • android/duckAiDataClearing.js
  • android/pages/duckplayer/dist/index.js

File has changed

Apple
    - apple/contentScope.js
  • apple/contentScopeIsolated.js
  • apple/duckAiChatHistory.js
  • apple/duckAiDataClearing.js
  • apple/pages/duckplayer/dist/index.js
  • apple/pages/duckplayer/index.html
  • apple/pages/history/dist/index.js
  • apple/pages/new-tab/dist/index.js
  • apple/pages/onboarding/dist/index.js
  • apple/pages/release-notes/dist/index.js
  • apple/pages/special-error/dist/index.js
  • apple/pages/special-error/index.html

File has changed

Chrome-mv3
    - chrome-mv3/inject.js

File has changed

Firefox
    - firefox/inject.js

File has changed

Integration
    - integration/contentScope.js
  • integration/pages/duckplayer/dist/index.js
  • integration/pages/example/dist/index.js
  • integration/pages/history/dist/index.js
  • integration/pages/new-tab/dist/index.js
  • integration/pages/onboarding/dist/index.js
  • integration/pages/release-notes/dist/index.js
  • integration/pages/special-error/dist/index.js

File has changed

Windows
    - windows/contentScope.js
  • windows/pages/duckplayer/dist/index.js
  • windows/pages/history/dist/index.js
  • windows/pages/new-tab/dist/index.js
  • windows/pages/onboarding/dist/index.js
  • windows/pages/release-notes/dist/index.js
  • windows/pages/special-error/dist/index.js
  • windows/pages/special-error/index.html

File has changed

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Cursor review was not successful.

This PR requires a manual review and approval from a member of one of the following teams:

  • @duckduckgo/content-scope-scripts-owners
  • @duckduckgo/apple-devs
  • @duckduckgo/android-devs
  • @duckduckgo/team-windows-development
  • @duckduckgo/extension-owners
  • @duckduckgo/config-aor
  • @duckduckgo/breakage-aor
  • @duckduckgo/breakage

@github-actions

github-actions Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Build Branch

Branch pr-releases/balint/feature/passkey-autofill
Commit 672040ec74
Updated June 2, 2026 at 12:02:42 PM UTC

Static preview entry points

QR codes (mobile preview)
Entry point QR code
Docs QR for docs preview
Static pages QR for static pages preview
Integration pages QR for integration pages preview

Integration commands

npm (Android / Extension):

npm i github:duckduckgo/content-scope-scripts#pr-releases/balint/feature/passkey-autofill

Swift Package Manager (Apple):

.package(url: "https://github.com/duckduckgo/content-scope-scripts.git", branch: "pr-releases/balint/feature/passkey-autofill")

git submodule (Windows):

git -C submodules/content-scope-scripts fetch origin pr-releases/balint/feature/passkey-autofill
git -C submodules/content-scope-scripts checkout origin/pr-releases/balint/feature/passkey-autofill
Pin to exact commit

npm (Android / Extension):

npm i github:duckduckgo/content-scope-scripts#672040ec7473e74f4ed7244f5623c19830cc3e95

Swift Package Manager (Apple):

.package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "672040ec7473e74f4ed7244f5623c19830cc3e95")

git submodule (Windows):

git -C submodules/content-scope-scripts fetch origin pr-releases/balint/feature/passkey-autofill
git -C submodules/content-scope-scripts checkout 672040ec7473e74f4ed7244f5623c19830cc3e95

Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Caller's options object mutated by event handler
    • The passkey-selected path now builds a shallow-copied options object (including publicKey) before modifying allowCredentials and removing mediation, avoiding mutation of the caller’s input.
  • ✅ Fixed: Repeated conditional calls silently orphan pending promises
    • Before storing a new conditional request, any existing pending request is now rejected with an AbortError and cleared so no promise is orphaned.

Create PR

Or push these changes by commenting:

@cursor push e63efb89aa
Preview (e63efb89aa)
diff --git a/injected/src/features/autofill-passkeys.js b/injected/src/features/autofill-passkeys.js
--- a/injected/src/features/autofill-passkeys.js
+++ b/injected/src/features/autofill-passkeys.js
@@ -29,17 +29,20 @@
                 for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
 
                 const options = /** @type {CredentialRequestOptions} */ (pendingOptions);
-                if (options.publicKey) {
-                    options.publicKey.allowCredentials = [{ type: CREDENTIAL_TYPE_PUBLIC_KEY, id: arr.buffer }];
-                }
-                delete options.mediation;
+                const passkeyOptions = {
+                    ...options,
+                    publicKey: options.publicKey
+                        ? { ...options.publicKey, allowCredentials: [{ type: CREDENTIAL_TYPE_PUBLIC_KEY, id: arr.buffer }] }
+                        : options.publicKey,
+                };
+                delete passkeyOptions.mediation;
 
                 const resolve = pendingResolve;
                 const reject = /** @type {(reason?: unknown) => void} */ (pendingReject);
                 pendingResolve = pendingReject = pendingOptions = null;
 
                 try {
-                    const credential = await savedOriginalGet(options);
+                    const credential = await savedOriginalGet(passkeyOptions);
                     resolve(credential);
                 } catch (e) {
                     reject(e);
@@ -47,25 +50,38 @@
             }
         });
 
-        this.wrapMethod(CredentialsContainer.prototype, 'get', /** @this {CredentialsContainer} */ function (originalGet, options) {
-            if (options?.mediation !== MEDIATION_CONDITIONAL) {
-                return originalGet.call(this, options);
-            }
+        this.wrapMethod(
+            CredentialsContainer.prototype,
+            'get',
+            /** @this {CredentialsContainer} */ function (originalGet, options) {
+                if (options?.mediation !== MEDIATION_CONDITIONAL) {
+                    return originalGet.call(this, options);
+                }
 
-            const rpId = options?.publicKey?.rpId || location.hostname;
+                if (pendingReject) {
+                    const abortError =
+                        typeof DOMException === 'function'
+                            ? new DOMException('The operation was aborted.', 'AbortError')
+                            : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
+                    pendingReject(abortError);
+                    pendingResolve = pendingReject = pendingOptions = null;
+                }
 
-            // @ts-expect-error windowsInteropPostMessage is a Windows-specific global
-            windowsInteropPostMessage({
-                Feature: MSG_OUTBOUND_FEATURE,
-                Name: MSG_OUTBOUND_NAME,
-                Data: { rpId },
-            });
+                const rpId = options?.publicKey?.rpId || location.hostname;
 
-            return new Promise(function (resolve, reject) {
-                pendingResolve = resolve;
-                pendingReject = reject;
-                pendingOptions = options;
-            });
-        });
+                // @ts-expect-error windowsInteropPostMessage is a Windows-specific global
+                windowsInteropPostMessage({
+                    Feature: MSG_OUTBOUND_FEATURE,
+                    Name: MSG_OUTBOUND_NAME,
+                    Data: { rpId },
+                });
+
+                return new Promise(function (resolve, reject) {
+                    pendingResolve = resolve;
+                    pendingReject = reject;
+                    pendingOptions = options;
+                });
+            },
+        );
     }
 }

Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Errors before try/catch leave pending promise hanging forever
    • I moved pending callback capture/cleanup before risky parsing and wrapped base64 decode plus option preparation in the existing try/catch so malformed credential IDs now reject the pending promise instead of leaving it unresolved.

Create PR

Or push these changes by commenting:

@cursor push 181b559127
Preview (181b559127)
diff --git a/injected/src/features/autofill-passkeys.js b/injected/src/features/autofill-passkeys.js
--- a/injected/src/features/autofill-passkeys.js
+++ b/injected/src/features/autofill-passkeys.js
@@ -24,21 +24,21 @@
             if (!pendingResolve) return;
 
             if (event.data?.type === MSG_INBOUND_PASSKEY_SELECTED) {
-                const raw = atob(event.data.credentialId);
-                const arr = new Uint8Array(raw.length);
-                for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
-
-                const options = /** @type {CredentialRequestOptions} */ (pendingOptions);
-                if (options.publicKey) {
-                    options.publicKey.allowCredentials = [{ type: CREDENTIAL_TYPE_PUBLIC_KEY, id: arr.buffer }];
-                }
-                delete options.mediation;
-
                 const resolve = pendingResolve;
                 const reject = /** @type {(reason?: unknown) => void} */ (pendingReject);
+                const options = /** @type {CredentialRequestOptions} */ (pendingOptions);
                 pendingResolve = pendingReject = pendingOptions = null;
 
                 try {
+                    const raw = atob(event.data.credentialId);
+                    const arr = new Uint8Array(raw.length);
+                    for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
+
+                    if (options.publicKey) {
+                        options.publicKey.allowCredentials = [{ type: CREDENTIAL_TYPE_PUBLIC_KEY, id: arr.buffer }];
+                    }
+                    delete options.mediation;
+
                     const credential = await savedOriginalGet(options);
                     resolve(credential);
                 } catch (e) {
@@ -47,25 +47,29 @@
             }
         });
 
-        this.wrapMethod(CredentialsContainer.prototype, 'get', /** @this {CredentialsContainer} */ function (originalGet, options) {
-            if (options?.mediation !== MEDIATION_CONDITIONAL) {
-                return originalGet.call(this, options);
-            }
+        this.wrapMethod(
+            CredentialsContainer.prototype,
+            'get',
+            /** @this {CredentialsContainer} */ function (originalGet, options) {
+                if (options?.mediation !== MEDIATION_CONDITIONAL) {
+                    return originalGet.call(this, options);
+                }
 
-            const rpId = options?.publicKey?.rpId || location.hostname;
+                const rpId = options?.publicKey?.rpId || location.hostname;
 
-            // @ts-expect-error windowsInteropPostMessage is a Windows-specific global
-            windowsInteropPostMessage({
-                Feature: MSG_OUTBOUND_FEATURE,
-                Name: MSG_OUTBOUND_NAME,
-                Data: { rpId },
-            });
+                // @ts-expect-error windowsInteropPostMessage is a Windows-specific global
+                windowsInteropPostMessage({
+                    Feature: MSG_OUTBOUND_FEATURE,
+                    Name: MSG_OUTBOUND_NAME,
+                    Data: { rpId },
+                });
 
-            return new Promise(function (resolve, reject) {
-                pendingResolve = resolve;
-                pendingReject = reject;
-                pendingOptions = options;
-            });
-        });
+                return new Promise(function (resolve, reject) {
+                    pendingResolve = resolve;
+                    pendingReject = reject;
+                    pendingOptions = options;
+                });
+            },
+        );
     }
 }

Comment thread injected/src/features/autofill-passkeys.js Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Web Compatibility Assessment

  • injected/src/features/autofill-passkeys.js L50-L69 error: The implementation stores only one global pending request (pendingResolve/pendingReject/pendingOptions). Concurrent navigator.credentials.get({ mediation: 'conditional' }) calls overwrite each other, which can leave earlier Promises unresolved or resolve the wrong caller.
  • injected/src/features/autofill-passkeys.js L23-L47 and L62-L69 error: The wrapped method has no completion path except passkeySelected. If native sends no message, sends a different message (e.g., cancel), or payload processing throws, the page Promise never settles, causing a hard hang in auth flows.
  • injected/src/features/autofill-passkeys.js L57-L65 and L62-L69 warning: windowsInteropPostMessage is called before pending handlers are registered. If the response can arrive synchronously/very early, the listener drops it (!pendingResolve) and the Promise remains pending.
  • injected/src/features/autofill-passkeys.js L31-L35 warning: The code mutates caller-provided options in place (allowCredentials assignment + delete options.mediation). If the app passes frozen/immutable options objects, this can throw and break credential flows.

Security Assessment

  • injected/src/features/autofill-passkeys.js L23-L47 error: Inbound interop messages are trusted based only on event.data?.type === 'passkeySelected' with no request ID / nonce correlation. A stale or spoofed message can be applied to the current pending request, violating request integrity.
  • injected/src/features/autofill-passkeys.js L27-L31 warning: credentialId is decoded without schema/type validation and without a guarded failure path. Malformed payloads can throw before state cleanup, creating a persistent DoS condition (stuck pending request).

Risk Level

High Risk: This PR introduces a new CredentialsContainer.get override on Windows plus a native-message-driven control path; current request lifecycle/correlation gaps can cause both auth-flow breakage and message-integrity issues.

Recommendations

  1. Replace single pending state with per-request tracking (Map<requestId, {resolve,reject,options}>) and include requestId in both outbound registerPasskeyRequest and inbound passkeySelected payloads.
  2. Add explicit settle paths for cancel/timeout/error and always clear pending state in finally to avoid unresolved Promises.
  3. Register pending request state before posting to native, or buffer inbound events until registration is complete.
  4. Validate inbound payload shape (type, credentialId type/size/base64) before processing; reject the specific request on validation failure.
  5. Avoid mutating caller options; create a defensive cloned options object for the savedOriginalGet call.
  6. Add integration tests covering: concurrent conditional gets, user-cancel/no-response, malformed payload, and immutable options objects.
Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Intercepted promise ignores AbortSignal during pending phase
    • The conditional mediation interceptor now rejects immediately on pre-aborted signals and listens for abort while pending to reject and clean up instead of hanging or forwarding an already-aborted signal to native get.

Create PR

Or push these changes by commenting:

@cursor push 98e385c5ef
Preview (98e385c5ef)
diff --git a/injected/src/features/autofill-passkeys.js b/injected/src/features/autofill-passkeys.js
--- a/injected/src/features/autofill-passkeys.js
+++ b/injected/src/features/autofill-passkeys.js
@@ -16,6 +16,10 @@
         let pendingReject = null;
         /** @type {CredentialRequestOptions | null} */
         let pendingOptions = null;
+        /** @type {AbortSignal | null} */
+        let pendingSignal = null;
+        /** @type {(() => void) | null} */
+        let pendingAbortHandler = null;
 
         const savedOriginalGet = navigator.credentials.get.bind(navigator.credentials);
 
@@ -34,6 +38,11 @@
                 }
                 delete options.mediation;
 
+                if (pendingSignal && pendingAbortHandler) {
+                    pendingSignal.removeEventListener('abort', pendingAbortHandler);
+                }
+                pendingSignal = pendingAbortHandler = null;
+
                 const resolve = pendingResolve;
                 const reject = /** @type {(reason?: unknown) => void} */ (pendingReject);
                 pendingResolve = pendingReject = pendingOptions = null;
@@ -47,25 +56,50 @@
             }
         });
 
-        this.wrapMethod(CredentialsContainer.prototype, 'get', /** @this {CredentialsContainer} */ function (originalGet, options) {
-            if (options?.mediation !== MEDIATION_CONDITIONAL || !options?.publicKey) {
-                return originalGet.call(this, options);
-            }
+        this.wrapMethod(
+            CredentialsContainer.prototype,
+            'get',
+            /** @this {CredentialsContainer} */ function (originalGet, options) {
+                if (options?.mediation !== MEDIATION_CONDITIONAL || !options?.publicKey) {
+                    return originalGet.call(this, options);
+                }
 
-            const rpId = options?.publicKey?.rpId || location.hostname;
+                if (options.signal?.aborted) {
+                    return Promise.reject(options.signal.reason || new DOMException('The operation was aborted.', 'AbortError'));
+                }
 
-            // @ts-expect-error windowsInteropPostMessage is a Windows-specific global
-            windowsInteropPostMessage({
-                Feature: MSG_OUTBOUND_FEATURE,
-                Name: MSG_OUTBOUND_NAME,
-                Data: { rpId },
-            });
+                const rpId = options?.publicKey?.rpId || location.hostname;
 
-            return new Promise(function (resolve, reject) {
-                pendingResolve = resolve;
-                pendingReject = reject;
-                pendingOptions = options;
-            });
-        });
+                // @ts-expect-error windowsInteropPostMessage is a Windows-specific global
+                windowsInteropPostMessage({
+                    Feature: MSG_OUTBOUND_FEATURE,
+                    Name: MSG_OUTBOUND_NAME,
+                    Data: { rpId },
+                });
+
+                return new Promise(function (resolve, reject) {
+                    pendingResolve = resolve;
+                    pendingReject = reject;
+                    pendingOptions = options;
+                    pendingSignal = options.signal || null;
+
+                    if (pendingSignal) {
+                        pendingAbortHandler = function () {
+                            if (pendingSignal && pendingAbortHandler) {
+                                pendingSignal.removeEventListener('abort', pendingAbortHandler);
+                            }
+                            pendingSignal = pendingAbortHandler = null;
+
+                            if (!pendingReject) return;
+                            const pendingAbortReject = pendingReject;
+                            pendingResolve = pendingReject = pendingOptions = null;
+                            pendingAbortReject(options.signal?.reason || new DOMException('The operation was aborted.', 'AbortError'));
+                        };
+
+                        pendingSignal.addEventListener('abort', pendingAbortHandler, { once: true });
+                    }
+                });
+            },
+        );
     }
 }

Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
@dbajpeyi

Copy link
Copy Markdown
Contributor

Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
@quanganhdo
quanganhdo removed their request for review April 22, 2026 21:47
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js
The suffix check cannot account for Related Origin Requests (e.g., facebook.com requesting passkeys for meta.com via .well-known/webauthn) or other valid cross-domain scenarios. Native has the full context for RP ID validation including public suffix and ROR checks.
If crypto.randomUUID is missing and no requestId can be generated, delegate to the original credentials.get() instead of proceeding without request correlation.
Add isSecureContext check to the pass-through guard. WebAuthn requires a secure context, so there's no point intercepting and sending registerPasskeyRequest to native when it can never succeed.
Comment thread injected/src/features/autofill-passkeys.js
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js
Comment thread injected/src/captured-globals.js
Comment thread injected/src/features/autofill-passkeys.js
Comment thread injected/src/features/autofill-passkeys.js
Comment thread injected/src/features/autofill-passkeys.js Outdated
@GioSensation

Copy link
Copy Markdown
Member

@gkerenyi @BalintFarkas Is this ready for another review by a human?

@BalintFarkas

Copy link
Copy Markdown
Contributor Author

@gkerenyi @BalintFarkas Is this ready for another review by a human?

@GioSensation Yes - requested it on the Asana PR for easier inbox management. @dbajpeyi says he'll get to it today.
https://app.asana.com/1/137249556945/task/1213790084159776/comment/1214385468127028?focus=true

Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features/autofill-passkeys.js Outdated
Comment thread injected/src/features.js

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web Compatibility Assessment

No new non-duplicate web-compat finding from the latest sync. Routing passkeySelected through the typed messaging API removes the earlier raw Windows interop concern and adds request correlation via requestId.

Previously-posted API-fidelity concerns around synchronous WebAuthn dictionary reads and delayed/shallow option snapshotting still look like the key remaining compat risks; I’m not re-posting those inline.

Security Assessment

No new non-duplicate security finding from the latest sync. The outbound message now sends only explicit { rpId, requestId } fields, so there is no nativeData spread, and inbound handling checks credentialId plus matching requestId before narrowing allowCredentials.

Risk Level

Critical Risk under the review rubric because this PR adds a new navigator.credentials.get() API override and changes captured-globals.js.

Recommendations

Keep the messaging route. Before merge, add/verify focused coverage for rejected-Promise semantics when option getters throw, immutable call-time snapshots for BufferSource fields such as challenge, and mismatched requestId subscription messages being ignored.

Verification: npm run build-surrogates --workspace=injected && npm run tsc-strict-core passes.

Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

navigator.credentials.get() is a WebIDL promise-returning operation, so exceptions thrown while reading the options dictionary (e.g. a throwing publicKey/mediation getter) surface as a rejected promise rather than a synchronous throw. The interception wrapper read those getters synchronously, so a throwing getter propagated out of get() synchronously and broke callers relying on .catch()/await. Read the discriminator getters under try/catch in the wrapper and snapshot the publicKey fields inside the returned promise so all such throws reject instead.

Also exclude the Windows-only autofillPasskeys feature from the integration entry point's enabled features (alongside autofillImport/brokerProtection): it globally wraps navigator.credentials.get and isn't meaningfully exercised in the mocked-messaging integration context.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web Compatibility Assessment

  • injected/src/features/autofill-passkeys.js lines 51-60 and 82-96, severity: info. The latest sync resolves the previously posted synchronous-throw compatibility concern: options.mediation / options.publicKey getter failures are now caught and returned as CapturedPromise.reject(...), and publicKey field reads happen inside the returned Promise executor so throwing getters reject the promise instead of escaping synchronously.

No new non-duplicate web-compat finding from this sync. The older delayed/shallow WebAuthn snapshotting concern remains the main residual API-fidelity risk, but I’m not re-posting it as a duplicate.

Security Assessment

No new non-duplicate security finding from this sync. The outbound notify path still sends explicit { rpId, requestId } fields only, and inbound passkeySelected handling keeps the request-id match before invoking the narrowed native credentials.get() call.

Risk Level

Critical Risk under the review rubric because this PR adds a new CredentialsContainer.prototype.get override and changes captured-globals.js, even though production enablement is Windows-scoped.

Recommendations

  1. Add/keep focused tests for rejected-Promise semantics when getters throw on mediation, publicKey, publicKey.rpId, and publicKey.challenge.
  2. Add/keep coverage for immutable call-time snapshots of WebAuthn BufferSource fields such as challenge, plus mismatched requestId subscription messages being ignored.

Verification: npm run build-surrogates --workspace=injected && npm run tsc-strict-core passes.

Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d1829c5. Configure here.

Comment thread injected/src/features/autofill-passkeys.js
Comment thread injected/src/features/autofill-passkeys.js
@BalintFarkas
BalintFarkas enabled auto-merge June 2, 2026 14:26
@BalintFarkas
BalintFarkas added this pull request to the merge queue Jun 2, 2026
Merged via the queue into main with commit 691afab Jun 2, 2026
37 of 38 checks passed
@BalintFarkas
BalintFarkas deleted the balint/feature/passkey-autofill branch June 2, 2026 14:34
jonathanKingston added a commit that referenced this pull request Jun 11, 2026
Add Jasmine tests for the Windows passkey interception flow merged in
#2571, covering request registration, native message correlation, abort
handling, supersede behavior, and credentials.get pass-through guards.

Fix ESM import paths in autofill-passkeys.js so the feature can be loaded
by the unit test runner (no behavior change).

Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-minor New feature — triggers minor version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants