Firefox · DOM
CVE-2026-109780
Logic Error in DOM
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
fordocshell/base/CanonicalBrowsingContext.cpp |
modified | |
ifdom/base/nsGlobalWindowOuter.cpp |
modified | |
iftesting/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js |
modified |
Files Changed
docshell/base/BrowsingContext.cppdocshell/base/BrowsingContext.hdocshell/base/CanonicalBrowsingContext.cppdocshell/base/nsDocShell.cppdom/base/nsGlobalWindowOuter.cpptesting/web-platform/tests/html/semantics/embedded-content/the-iframe-element/resources/sandbox-top-close-iframe.htmltesting/web-platform/tests/html/semantics/embedded-content/the-iframe-element/sandbox-top-close.tentative.sub.window.js
Patch
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…
References
On This Page