Released May 11, 2026

CVE-2026-28942

CVSS 6.5 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: Milad Nasr and Nicholas Carlini with Claude, Anthropic

Key insight. The dialog close command used a value string tied to the invoker button’s attribute while dispatching author-observable events that can mutate that attribute; snapshotting the value into a local before the events fire removes the dangling use.

Patch Details

diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
new file mode 100644
index 000000000000..d6d0936dfab3
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
@@ -0,0 +1,4 @@
+This tests removing the value attribute of a button as it tries to close the dialog element.
+WebKit should not crash or hit any assertions under ASAN.
+
+PASS
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash.html b/LayoutTests/fast/html/dialog-close-from-button-crash.html
new file mode 100644
index 000000000000..ac956c9a8823
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>This tests removing the value attribute of a button as it tries to close the dialog element.<br>
+WebKit should not crash or hit any assertions under ASAN.</p>
+<dialog id=dialog open>
+    <button command=close commandfor=dialog>Close</button>
+</dialog>
+<script>
+window?.testRunner?.dumpAsText();
+window?.GCController?.collect();
+const button = document.querySelector('button');
+button.setAttribute('value', 'PA' + 'SS');
+document.querySelector('dialog').addEventListener('beforetoggle', (event) => {
+    button.removeAttribute('value');
+});
+button.click();
+document.write(dialog.returnValue);
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/html/HTMLDialogElement.cpp b/Source/WebCore/html/HTMLDialogElement.cpp
index 0b39c2a2f885..e0ef22618ef9 100644
--- a/Source/WebCore/html/HTMLDialogElement.cpp
+++ b/Source/WebCore/html/HTMLDialogElement.cpp
@@ -305,11 +305,13 @@ bool HTMLDialogElement::handleCommandInternal(HTMLButtonElement& invoker, const
 
     if (isOpen()) {
         if (command == CommandType::Close) {
-            close(invoker.value().string(), &invoker);
+            String value = invoker.value().string();
+            close(value, &invoker);
             return true;
         }
         if (command == CommandType::RequestClose) {
-            requestClose(invoker.value().string(), &invoker);
+            String value = invoker.value().string();
+            requestClose(value, &invoker);
             return true;
         }
     } else {

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-28942
Bugzilla 312180
Component WebCore
Bug Class UAF
Severity medium
Commit 70753442a3d86ab5…
Advisory Apple Advisory

Terminology

  • Invoker commands — HTML button command/commandfor lets a button drive a dialog (close/requestClose); the button’s value is passed as the dialog returnValue.
  • beforetoggle event — An author-observable event fired as a dialog opens/closes; its handler can run arbitrary script and mutate the DOM mid-operation.
  • Attribute-backed string lifetime — A string taken from an element’s attribute can be invalidated if the attribute is changed while the string is still in use.

Root Cause Analysis

This fixes a use-after-free (observed as a crash / ASan hit) in HTMLDialogElement command handling by snapshotting the invoker’s value string before dispatching author-observable events. HTMLDialogElement::handleCommandInternal, for the Close and RequestClose commands, called close(invoker.value().string(), &invoker) / requestClose(invoker.value().string(), &invoker) — passing a string derived from the invoker button’s value attribute directly as an argument while also passing &invoker. close()/requestClose() dispatch events (e.g. beforetoggle/close) that run author script; that script can mutate or remove the invoker button’s value attribute during the call. Because the string was obtained from the invoker’s attribute storage as part of the call, author code mutating that attribute mid-close can invalidate the underlying string data the operation is still using, yielding a use-after-free of the attribute-backed string. The fix hoists the value into an owned local first (String value = invoker.value().string();) and passes that local to close(value, &invoker) / requestClose(value, &invoker), so the value is captured before any event fires and cannot be invalidated by author script that alters the invoker during the close sequence. The restored invariant is that the command value is a stable snapshot taken before author-observable side effects run. The regression test opens a dialog, and in a beforetoggle handler removes the button’s value attribute while the close command runs, expecting no crash and PASS returnValue.

Attack Path

1. Set up a dialog + invoker

Create an open <dialog> with a <button command=close commandfor=dialog> and a value attribute.

2. Hook the close event

Add a beforetoggle listener that removes/mutates the button’s value attribute.

3. Invoke close

Click the button so handleCommandInternal reads invoker.value().string() and calls close(), firing beforetoggle.

4. Free during use

The handler mutates the invoker’s value attribute mid-close, invalidating the in-use string — a use-after-free in WebContent.

Impact Assessment

A use-after-free in the WebContent process reachable from ordinary HTML/JS by mutating the invoker during a dialog close. Event-driven mutate-during-use UAFs are controllable with heap grooming; the advisory rates it a crash, and the class can escalate toward memory disclosure/corruption in WebContent.

Audit Directions

Same file: other command handlers

Audit HTMLDialogElement and other command/invoker handlers for element-derived strings passed into calls that dispatch events; snapshot before dispatch.

Mutate-during-event pattern

Grep for value()/getAttribute() results passed directly into methods that fire events (dispatchEvent, toggle, focus) where author script can invalidate them.

Changed Functions

Function File Change Note
HTMLDialogElement::handleCommandInternal Source/WebCore/html/HTMLDialogElement.cpp modified Copies invoker.value().string() into a local String before calling close()/requestClose(), so author script that mutates the invoker during the close cannot invalidate the value in use.

Files Changed

Source Files

  • Source/WebCore/html/HTMLDialogElement.cpp

Test Files

  • LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
  • LayoutTests/fast/html/dialog-close-from-button-crash.html

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
new file mode 100644
index 000000000000..d6d0936dfab3
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
@@ -0,0 +1,4 @@
+This tests removing the value attribute of a button as it tries to close the dialog element.
+WebKit should not crash or hit any assertions under ASAN.
+
+PASS
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash.html b/LayoutTests/fast/html/dialog-close-from-button-crash.html
new file mode 100644
index 000000000000..ac956c9a8823
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>This tests removing the value attribute of a button as it tries to close the dialog element.<br>
+WebKit should not crash or hit any assertions under ASAN.</p>
+<dialog id=dialog open>
+    <button command=close commandfor=dialog>Close</button>
+</dialog>
+<script>
+window?.testRunner?.dumpAsText();
+window?.GCController?.collect();
+const button = document.querySelector('button');
+button.setAttribute('value', 'PA' + 'SS');
+document.querySelector('dialog').addEventListener('beforetoggle', (event) => {
+    button.removeAttribute('value');
+});
+button.click();
+document.write(dialog.returnValue);
+</script>
+</body>
+</html>