Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionMitigation bypass in the DOM: Core & HTML component
ComponentDOM
Bug ClassLogic Error
Tracker2072775
Fix commit24046b374491 (firefox) +117/-31
CISA KEVNot listed
CreditedShannon Booth
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
for
docshell/base/CanonicalBrowsingContext.cpp
modified
if
dom/base/nsGlobalWindowOuter.cpp
modified
if
testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
modified

Files Changed

  • docshell/base/BrowsingContext.cpp
  • docshell/base/BrowsingContext.h
  • docshell/base/CanonicalBrowsingContext.cpp
  • docshell/base/nsDocShell.cpp
  • dom/base/nsGlobalWindowOuter.cpp
  • testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html
  • testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp
index e2e45531cd2..bcfcb767e89 100644
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -315,6 +315,11 @@ bool BrowsingContext::IsOwnedByProcess() const {
          !nsDocShell::Cast(mDocShell)->WillChangeProcess();
 }
 
+bool BrowsingContext::IsScriptClosable() const {
+  return GetTopLevelCreatedByWebContent() ||
+         (mChildSessionHistory && mChildSessionHistory->Count() == 1);
+}
+
 bool BrowsingContext::SameOriginWithTop() {
   MOZ_ASSERT(IsInProcess());
   // If the top BrowsingContext is not same-process to us, it is cross-origin
@@ -2171,20 +2176,31 @@ bool BrowsingContext::RemoveRootFromBFCacheSync() {
   return false;
 }
 
-nsresult BrowsingContext::CheckSandboxFlags(nsDocShellLoadState* aLoadState) {
+nsresult BrowsingContext::EnsureSourceSandboxAllowsNavigation(
+    nsDocShellLoadState* aLoadState, bool aForClose) {
   const auto& sourceBC = aLoadState->SourceBrowsingContext();
   if (sourceBC.IsNull()) {
     return NS_OK;
   }
+  return EnsureSourceSandboxAllowsNavigation(sourceBC.GetMaybeDiscarded(),
+                                             aForClose);
+}
 
+nsresult BrowsingContext::EnsureSourceSandboxAllowsNavigation(
+    BrowsingContext* aSourceBC, bool aForClose) {
   // We might be called after the source BC has been discarded, but before we've
   // destroyed our in-process instance of the BrowsingContext object in some
   // situations (e.g. after creating a new pop-up with window.open while the
   // window is being closed). In these situations we want to still perform the
   // sandboxing check against our in-process copy. If we've forgotten about the
   // context already, assume it is sanboxed. (bug 1643450)
-  BrowsingContext* bc = sourceBC.GetMaybeDiscarded();
-  if (!bc || bc->IsSandboxedFrom(this)) {
+  if (!aSourceBC || aSourceBC->IsSandboxedFrom(this)) {
+    nsPrintfCString msg(
+        "Blocked attempt to %s another window from a sandboxed frame.",
+        aForClose ? "close" : "navigate");
+    nsContentUtils::ReportToConsoleNonLocalized(
+        NS_ConvertUTF8toUTF16(msg), nsIScriptError::errorFlag, "Window"_ns,
+        aSourceBC ? aSourceBC->GetExtantDocument() : nullptr);
     return NS_ERROR_DOM_SECURITY_ERR;
   }
   return NS_OK;
@@ -2314,7 +2330,7 @@ nsresult BrowsingContext::LoadURI(nsDocShellLoadState* aLoadState,
   // triggering the load, and we don't want the target process to have to trust
   // the triggering process to do the appropriate checks for the
   // BrowsingContext's sandbox flags.
-  MOZ_TRY(CheckSandboxFlags(aLoadState));
+  MOZ_TRY(EnsureSourceSandboxAllowsNavigation(aLoadState));
   SetTriggeringAndInheritPrincipals(aLoadState->TriggeringPrincipal(),
                                     aLoadState->PrincipalToInherit(),
                                     aLoadState->GetLoadIdentifier());
@@ -2475,7 +2491,7 @@ nsresult BrowsingContext::InternalLoad(nsDocShellLoadState* aLoadState) {
   // triggering the load, and we don't want the target process to have to trust
   // the triggering process to do the appropriate checks for the
   // BrowsingContext's sandbox flags.
-  MOZ_TRY(CheckSandboxFlags(aLoadState));
+  MOZ_TRY(EnsureSourceSandboxAllowsNavigation(aLoadState));
 
   const auto& sourceBC = aLoadState->SourceBrowsingContext();
 
@@ -2736,10 +2752,20 @@ void BrowsingContext::Close(CallerType aCallerType, ErrorResult& aError) {
     return;
   }
 
+  if (RefPtr<nsGlobalWindowInner> callerInner =
+          nsContentUtils::IncumbentInnerWindow()) {
+    if (BrowsingContext* callerBC = callerInner->GetBrowsingContext()) {
+      if (NS_FAILED(EnsureSourceSandboxAllowsNavigation(callerBC, true))) {
+        return;
+      }
+    }
+  }
+
   // This is a bit of a hack for webcompat. Content needs to see an updated
   // |window.closed| value as early as possible, so we set this before we
   // actually send the DOMWindowClose event, which happens in the process where
   // the document for this browsing context is loaded.
+  // XXX bug 2074011, close might fail / get canceled but closed stays true.
   MOZ_ALWAYS_SUCCEEDS(SetClosed(true));
 
   if (ContentChild* cc = ContentChild::GetSingleton()) {
diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h
index 554824a82ee..1e55e50cb62 100644
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -409,6 +409,9 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
   // message.
   bool IsDiscarded() const { return mIsDiscarded; }
 
+  // https://html.spec.whatwg.org/#script-closable
+  bool IsScriptClosable() const;
+
   // Returns true if none of the BrowsingContext's ancestor BrowsingContexts or
   // WindowContexts are discarded or cached.
   bool AncestorsAreCurrent() const;
@@ -491,10 +494,13 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
   // if it is cached, and returns true if it was.
   bool RemoveRootFromBFCacheSync();
 
-  // If the load state includes a source BrowsingContext has been passed, check
-  // to see if we are sandboxed from it as the result of an iframe or CSP
-  // sandbox.
-  nsresult CheckSandboxFlags(nsDocShellLoadState* aLoadState);
+  // Check to see if we are sandboxed from the source BrowsingContext as the
+  // result of an iframe or CSP sandbox. Reports a console error if so.
+  // This also applies to window.close(), aForClose adjusts the error.
+  nsresult EnsureSourceSandboxAllowsNavigation(BrowsingContext* aSourceBC,
+                                               bool aForClose = false);
+  nsresult EnsureSourceSandboxAllowsNavigation(nsDocShellLoadState* aLoadState,
+                                               bool aForClose = false);
 
   // If the current BrowsingContext is top-level, we run checks to see if
   // the source BrowsingContext is allowed to perform the navigation.
diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp
index 999a7744f1a..dbe57b9126d 100644
--- a/docshell/base/CanonicalBrowsingContext.cpp
+++ b/docshell/base/CanonicalBrowsingContext.cpp
@@ -1642,8 +1642,9 @@ Maybe<int32_t> CanonicalBrowsingContext::HistoryGo(
   }
 
   for (auto& loadResult : loadResults) {
-    if (nsresult result = loadResult.mBrowsingContext->CheckSandboxFlags(
-            loadResult.mLoadState);
+    if (nsresult result =
+            loadResult.mBrowsingContext->EnsureSourceSandboxAllowsNavigation(
+                loadResult.mLoadState);
         NS_FAILED(result)) {
       aResolver(result);
       MOZ_LOG(gSHLog, LogLevel::Debug,
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index 3a2468ae83b..7b7f023f19e 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -8594,7 +8594,7 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
   // available in the process triggering the load, and we don't want the target
   // process to have to trust the triggering process to do the appropriate
   // checks for the BrowsingContext's sandbox flags.
-  MOZ_TRY(mBrowsingContext->CheckSandboxFlags(aLoadState));
+  MOZ_TRY(mBrowsingContext->EnsureSourceSandboxAllowsNavigation(aLoadState));
   MOZ_TRY(mBrowsingContext->CheckFramebusting(aLoadState));
 
   NS_ENSURE_STATE(!HasUnloadedParent());
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
index 19727359724..a287649849c 100644
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -6047,6 +6047,17 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
     return;
   }
 
+  // Check allow-top-navigation sandbox flags
+  if (RefPtr<nsGlobalWindowInner> callerInner =
+          nsContentUtils::IncumbentInnerWindow()) {
+    if (BrowsingContext* callerBC = callerInner->GetBrowsingContext()) {
+      if (NS_FAILED(mBrowsingContext->EnsureSourceSandboxAllowsNavigation(
+              callerBC, true))) {
+        return;
+      }
+    }
+  }
+
   // Don't allow scripts from content to close non-neterror windows that
   // were not opened by script.
   if (mDoc) {
@@ -6054,26 +6065,17 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) {
     nsresult rv = mDoc->GetURL(url);
     NS_ENSURE_SUCCESS_VOID(rv);
 
-    RefPtr<ChildSHistory> csh =
-        nsDocShell::Cast(mDocShell)->GetSessionHistory();
-
     if (!StringBeginsWith(url, u"about:neterror"_ns) &&
-        !mBrowsingContext->GetTopLevelCreatedByWebContent() &&
-        !aTrustedCaller && csh && csh->Count() > 1) {
-      bool allowClose =
-          mAllowScriptsToClose ||
-          Preferences::GetBool("dom.allow_scripts_to_close_windows", true);
-      if (!allowClose) {
-        // We're blocking the close operation
-        // report localized error msg in JS console
-        nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
-                                        "DOM Window"_ns,
-                                        mDoc,  // Better name for the category?
-                                        PropertiesFile::DOM_PROPERTIES,
-                                        "WindowCloseByScriptBlockedWarning");
-
-        return;
-      }
+        !mBrowsingContext->IsScriptClosable() && !aTrustedCaller &&
+        !mAllowScriptsToClose &&
+        !Preferences::GetBool("dom.allow_scripts_to_close_windows", true)) {
+      // We're blocking the close operation
+      // report localized error msg in JS console
+      nsContentUtils::ReportToConsole(
+          nsIScriptError::warningFlag, "DOM Window"_ns,
+          mDoc,  // Better name for the category?
+          PropertiesFile::DOM_PROPERTIES, "WindowCloseByScriptBlockedWarning");
+      return;
     }
   }
 
diff --git a/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html
new file mode 100644
index 00000000000..653bc1d3565
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<script>
+try { window.top.close(); } catch(e) {}
+window.parent.opener.postMessage('done', '*');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
new file mode 100644
index 00000000000..6afb88a99e1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
@@ -0,0 +1,46 @@
+// META: title=Sandboxed iframe top window close
+// META: script=/common/get-host-info.sub.js
+
+// Tentative because of https://github.com/whatwg/html/pull/12960
+
+'use strict';
+
+const IFRAME_PATH = '/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html';
+
+function testWithSandboxAttr(sandbox, xorigin = false) {
+  return new Promise(resolve => {
+    const popup = window.open('about:blank');
+
+    window.addEventListener('message', function handler(e) {
+      if (e.data !== 'done') return;
+      window.removeEventListener('message', handler);
+      const closed = popup.closed;
+      if (!closed) popup.close();
+      resolve(closed);
+    });
+
+    const iframe = popup.document.createElement('iframe');
+    iframe.sandbox = sandbox;
+    if (xorigin) {
+      iframe.src = get_host_info().HTTP_REMOTE_ORIGIN + IFRAME_PATH;
+    } else {
+      iframe.src = IFRAME_PATH;
+    }
+    popup.document.body.appendChild(iframe);
+  });
+}
+
+promise_test(async () => {
+  const closed = await testWithSandboxAttr('allow-scripts');
+  assert_false(closed);
+}, 'Sandboxed iframe without allow-top-navigation cannot close top window');
+
+promise_test(async () => {
+  const closed = await testWithSandboxAttr('allow-scripts allow-top-navigation');
+  assert_true(closed);
+}, 'Sandboxed iframe with allow-top-navigation can close top window');
+
+promise_test(async () => {
+  const closed = await testWithSandboxAttr('allow-scripts', true);
+  assert_false(closed);
+}, 'Cross-origin sandboxed iframe without allow-top-navigation cannot close top window');
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html
new file mode 100644
index 00000000000..653bc1d3565
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html
@@ -0,0 +1,5 @@
+<!doctype html>
+<script>
+try { window.top.close(); } catch(e) {}
+window.parent.opener.postMessage('done', '*');
+</script>
diff --git a/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
new file mode 100644
index 00000000000..6afb88a99e1
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
@@ -0,0 +1,46 @@
+// META: title=Sandboxed iframe top window close
+// META: script=/common/get-host-info.sub.js
+
+// Tentative because of https://github.com/whatwg/html/pull/12960
+
+'use strict';
+
+const IFRAME_PATH = '/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.html';
+
+function testWithSandboxAttr(sandbox, xorigin = false) {
+  return new Promise(resolve => {
+    const popup = window.open('about:blank');
+
+    window.addEventListener('message', function handler(e) {
+      if (e.data !== 'done') return;
+      window.removeEventListener('message', handler);
+      const closed = popup.closed;
+      if (!closed) popup.close();
+      resolve(closed);
+    });
+
+    const iframe = popup.document.createElement('iframe');
+    iframe.sandbox = sandbox;
+    if (xorigin) {
+      iframe.src = get_host_info().HTTP_REMOTE_ORIGIN + IFRAME_PATH;
+    } else {
+      iframe.src = IFRAME_PATH;
+    }
+    popup.document.body.appendChild(iframe);
+  });
+}
+
+promise_test(async () => {
+  const closed = await testWithSandboxAttr('allow-scripts');
+  assert_false(closed);
+}, 'Sandboxed iframe without allow-top-navigation cannot close top window');
+
+promise_test(async () => {
+  const closed = await testWithSandboxAttr('allow-scripts allow-top-navigation');
+  assert_true(closed);
+}, 'Sandboxed iframe with allow-top-navigation can close top window');
+
+promise_test(async () => {
+  const closed = await testWithSandboxAttr('allow-scripts', true);
+  assert_false(closed);
+}, 'Cross-origin sandboxed iframe without allow-top-navigation cannot close top window');
Loading diff…