From a096990aa26595596439ee779841cda1a01bd6c9 Mon Sep 17 00:00:00 2001
From: Fernando Fiori
Date: Fri, 11 Sep 2026 16:59:14 -0700
Subject: [PATCH 1/3] Base focus permission on the document that asks
The "focus-without-user-activation" feature asked whether the document being
focused was allowed to use it. That answers the wrong question in both
directions.
It does not stop the thing the feature exists to stop. A restricted frame that
wants to move the user's focus only has to aim at a document that is allowed:
calling parent.focus() from a restricted iframe is permitted today, because the
top-level document is always allowed under the 'self' default allowlist. The
check cannot bind when the caller chooses the target.
And it makes the TPAC 2024 resolution -- that a parent may programmatically
focus a child navigable -- impossible to express, because the restricted child's
own policy decides.
The allow focus steps now take the document responsible for the change, and
grant permission when that document is allowed to use the feature, has transient
activation, or already holds the focus. Holding the focus includes holding it in
a descendant navigable, so an embedded document can manage its own controls,
delegate into nested content, and take focus back from it; once focus leaves
that subtree the allowance ends.
Which document is responsible is determined at each call site rather than by
consulting an ambient global:
* focus() on an element, the autofocus steps, and the dialog and popover
focusing steps use the element's or subject's node document, which is what
they already passed. Reaching one of those elements requires same-origin
access to the document that contains it, so that document is both the
natural attribution and one the caller cannot forge.
* focus() on a Window uses the incumbent global object's associated document.
This is the only focus-producing API reachable cross-origin, so it is the
only one where the caller cannot be read off the arguments, and the only one
where the check has anything to bind against.
Deriving that one from the incumbent follows Location's navigate steps, which
obtain sourceDocument the same way. The algorithm itself consults no global, so
it remains well defined on paths that run without script on the stack -- notably
flush autofocus candidates, where the incumbent is explicitly unavailable.
An autofocus candidate is re-checked when candidates are flushed. A candidate
authorized while its document held the focus is discarded if that no longer
holds, so a queued autofocus cannot reclaim focus after it has been handed away.
For every call site other than Window.focus() this is a strict superset of the
previous behavior: the same document is passed, and the added steps only grant.
Window.focus() changes in both directions by design -- a restricted document can
no longer move focus by aiming at an allowed one, and an allowed document can now
delegate into a restricted child.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
source | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/source b/source
index 1d4de3a60e2..4f72f830033 100644
--- a/source
+++ b/source
@@ -87402,8 +87402,15 @@ dictionary CommandEventInit : EventInit {
If current is null, then return.
- If the allow focus steps given current's active document return false, then return.
+ If current's active document is not
+ fully active, then return.
+
+ Let focusSetterDocument be the incumbent global object's associated Document.
+
+ If the allow focus steps given focusSetterDocument return false,
+ then return.
Run the focusing steps with current.
@@ -87469,17 +87476,21 @@ dictionary CommandEventInit : EventInit {
The allow focus steps, given a Document object
- target, are:
+
focusSetterDocument, are:
- If target is allowed to use the "If focusSetterDocument is not fully active, then return
+ false.
+
+ If focusSetterDocument is allowed to use the "focus-without-user-activation" feature,
then return true.
- If target's relevant global object has transient
- activation, then return true.
+ If focusSetterDocument's relevant global object has
+ transient activation, then return true.
- Return false.
+ Return the result of running the has focus steps given
+ focusSetterDocument.
@@ -87638,6 +87649,9 @@ dictionary CommandEventInit : EventInit {
Remove element from
candidates.
+ If the allow focus steps given doc return false, then
+ continue.
+
Let inclusiveAncestorDocuments be a list consisting of the
active document of doc's inclusive
ancestor navigables.
@@ -162133,6 +162147,7 @@ INSERT INTERFACES HERE
Félix Sanz,
Felix Sasaki,
Fernando Altomare Serboncini,
+ Fernando Fiori,
Forbes Lindesay,
Francesco Schwarz,
Francis Brosnan Blazquez,
From c3c5175cd5286af6174c750124b084c8954c19e1 Mon Sep 17 00:00:00 2001
From: Fernando Fiori
Date: Fri, 11 Sep 2026 17:18:28 -0700
Subject: [PATCH 2/3] Confine the focus-derived allowance to the setter's own
navigable chain
The previous commit granted permission to whoever holds the focus, without
relating the destination to the document asking. Since focus() on a Window is
reachable cross-origin, that let a restricted document push the user's focus
into an unrelated cross-origin one -- something the previous text refuses.
T (top-level, allowed)
|- F (restricted, cross-origin)
\- V (restricted, cross-origin)
T delegates focus into F, which is the case this change exists to permit. F then
calls parent.frames['V'].focus(). F is not allowed to use the feature and has no
transient activation, but F holds the focus, so the allowance applied and the
focus landed in V. V's own policy was never consulted, and V is neither an
ancestor nor a descendant of F. Previously this is refused, because the check
was made against V. A single user click into F arms it the same way, and unlike
transient activation the allowance does not expire.
The allow focus steps now take the destination as well, and the focus-derived
step requires it to be on the setter's own chain -- in the setter's inclusive
ancestor navigables or its inclusive descendant navigables. A document that
holds the focus may therefore move it within its own subtree and hand it back
toward its embedders, but cannot place it sideways into an unrelated navigable.
The destination is an optional argument defaulting to the target, so the four
call sites where the document asking is the document being focused are
unchanged, and an external caller written against the one-argument form keeps
its previous meaning. Only focus() on a Window passes both, which is also the
only call site that can name a destination outside the caller's chain.
The first two grants are deliberately left unconditional. Being allowed to use
the feature, or having just been interacted with, are entitlements the
destination does not qualify; only the focus-derived grant is transferable, and
only it is confined.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
source | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/source b/source
index 4f72f830033..bf360f3a817 100644
--- a/source
+++ b/source
@@ -87409,7 +87409,8 @@ dictionary CommandEventInit : EventInit {
data-x="concept-incumbent-global">incumbent global object's associated Document.
- If the allow focus steps given focusSetterDocument return false,
+
If the allow focus steps given current's active document and focusSetterDocument return false,
then return.
Run the focusing steps with current.
@@ -87476,7 +87477,8 @@ dictionary CommandEventInit : EventInit {
The allow focus steps, given a Document object
- focusSetterDocument, are:
+
target and an optional
Document object
focusSetterDocument
+ (default
target), are:
If focusSetterDocument is not fully active, then return
@@ -87489,6 +87491,11 @@ dictionary CommandEventInit : EventInit {
If focusSetterDocument's relevant global object has
transient activation, then return true.
+ If neither focusSetterDocument's inclusive ancestor navigables nor
+ focusSetterDocument's inclusive descendant navigables contains target's node navigable, then
+ return false.
+
Return the result of running the has focus steps given
focusSetterDocument.
From 15c804358d1580ad30ad0630b57e75488b0c298c Mon Sep 17 00:00:00 2001
From: Fernando Fiori
Date: Tue, 15 Sep 2026 16:03:36 -0700
Subject: [PATCH 3/3] Revert "Confine the focus-derived allowance to the
setter's own navigable chain"
This reverts commit c3c5175cd5286af6174c750124b084c8954c19e1.
---
source | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/source b/source
index bf360f3a817..4f72f830033 100644
--- a/source
+++ b/source
@@ -87409,8 +87409,7 @@ dictionary CommandEventInit : EventInit {
data-x="concept-incumbent-global">incumbent global object's associated Document.
- If the allow focus steps given current's active document and focusSetterDocument return false,
+
If the allow focus steps given focusSetterDocument return false,
then return.
Run the focusing steps with current.
@@ -87477,8 +87476,7 @@ dictionary CommandEventInit : EventInit {
The allow focus steps, given a Document object
- target and an optional Document object focusSetterDocument
- (default target), are:
+
focusSetterDocument, are:
If focusSetterDocument is not fully active, then return
@@ -87491,11 +87489,6 @@ dictionary CommandEventInit : EventInit {
If focusSetterDocument's relevant global object has
transient activation, then return true.
- If neither focusSetterDocument's inclusive ancestor navigables nor
- focusSetterDocument's inclusive descendant navigables contains target's node navigable, then
- return false.
-
Return the result of running the has focus steps given
focusSetterDocument.