Firefox · Core
CVE-2026-84127
Logic Error in Core
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.ktmobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.ktmobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.ktmobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.ktmobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.ktmobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.ktmobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.ktmobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.ktmobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.ktmobile/android/android-components/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.ktmobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.ktmobile/android/focus-android/app/src/main/java/org/mozilla/focus/FocusApplication.ktmobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java
Patch
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt
index a6abfcb09a8..4168b63f02a 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt
@@ -152,8 +152,17 @@ class GeckoEngine(
}
}
private val webExtensionTabHandler = object : TabHandler {
- override fun onNewTab(webExtension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) {
- webExtensionDelegate?.onNewTab(webExtension, engineSession, active, url)
+ override fun isInPrivateBrowsing(): Boolean =
+ webExtensionDelegate?.isInPrivateBrowsing() ?: false
+
+ override fun onNewTab(
+ webExtension: WebExtension,
+ engineSession: EngineSession,
+ active: Boolean,
+ url: String,
+ isPrivate: Boolean,
+ ) {
+ webExtensionDelegate?.onNewTab(webExtension, engineSession, active, url, isPrivate)
}
override fun onOpenOptionsPage(extension: WebExtension) {
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt
index 8633cb91821..e611d576910 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt
@@ -260,8 +260,11 @@ class GeckoWebExtension(
ext: GeckoNativeWebExtension,
tabDetails: GeckoNativeWebExtension.CreateTabDetails,
): GeckoResult<GeckoSession>? {
+ // TODO bug 1372178: extensions cannot set (non-)privateness.
+ val isPrivate = tabHandler.isInPrivateBrowsing()
val geckoEngineSession = GeckoEngineSession(
runtime = runtime,
+ privateMode = isPrivate,
defaultSettings = defaultSettings,
openGeckoSession = false,
)
@@ -271,6 +274,7 @@ class GeckoWebExtension(
geckoEngineSession,
tabDetails.active == true,
tabDetails.url ?: "",
+ isPrivate,
)
return GeckoResult.fromValue(geckoEngineSession.geckoSession)
}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
index 376a205c8d1..19f680f72b8 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
@@ -1972,7 +1972,7 @@ class GeckoEngineTest {
tabDelegateCaptor.value.onNewTab(extension, createTabDetails)
val extensionCaptor = argumentCaptor<WebExtension>()
- verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""))
+ verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""), eq(false))
assertEquals(extId, extensionCaptor.value.id)
}
@@ -2076,7 +2076,7 @@ class GeckoEngineTest {
tabDelegateCaptor.value.onNewTab(extension, createTabDetails)
val extensionCaptor = argumentCaptor<WebExtension>()
- verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""))
+ verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""), eq(false))
assertEquals(extId, extensionCaptor.value.id)
}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
index 0115b874a8d..cd4f8f3731e 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
@@ -340,13 +340,54 @@ class GeckoWebExtensionTest {
// Verify that tab methods are forwarded to the handler
val tabDetails = mockCreateTabDetails(active = true, url = "url")
tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
- verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"))
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"), eq(false))
assertNotNull(engineSessionCaptor.value)
+ assertFalse(engineSessionCaptor.value.geckoSession.settings.usePrivateMode)
tabDelegateCaptor.value.onOpenOptionsPage(nativeGeckoWebExt)
verify(tabHandler).onOpenOptionsPage(eq(extension))
}
+ @Test
+ fun `global tab handler opens a private tab when private browsing mode is active`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ whenever(runtime.webExtensionController).thenReturn(mock())
+ val tabHandler: TabHandler = mock()
+ val tabDelegateCaptor = argumentCaptor<WebExtension.TabDelegate>()
+ val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
+
+ val nativeGeckoWebExt: WebExtension =
+ mockNativeWebExtension(id = "id", location = "uri", metaData = mockNativeWebExtensionMetaData())
+
+ // Create extension and register global tab handler
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ val defaultSettings: DefaultSettings = mock()
+
+ // Simulate that the user has enabled private browsing.
+ whenever(tabHandler.isInPrivateBrowsing()).thenReturn(true)
+
+ extension.registerTabHandler(tabHandler, defaultSettings)
+ verify(nativeGeckoWebExt).tabDelegate = tabDelegateCaptor.capture()
+
+ // Simulate browser.tabs.create() call from extension.
+ val tabDetails = mockCreateTabDetails(active = true, url = "url")
+ tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"), eq(true))
+ assertTrue(engineSessionCaptor.value.geckoSession.settings.usePrivateMode)
+
+ // Simulate that the user switched back to non-private browsing.
+ whenever(tabHandler.isInPrivateBrowsing()).thenReturn(false)
+
+ // Now browser.tabs.create() should open in a non-private browsing context.
+ tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"), eq(false))
+ assertFalse(engineSessionCaptor.value.geckoSession.settings.usePrivateMode)
+ }
+
@Test
fun `register session-specific tab handler`() {
val runtime: GeckoRuntime = mock()
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt
index 1c22cc1e5bb..ee98a9dbe1c 100644
--- a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt
@@ -302,6 +302,11 @@ interface MessageHandler {
* A handler for all tab related events (triggered by browser.tabs.* methods).
*/
interface TabHandler {
+ /**
+ * Invoked to determine the current private browsing mode. New tabs opened
+ * by extensions may use this state if not specified otherwise.
+ */
+ fun isInPrivateBrowsing(): Boolean = false
/**
* Invoked when a web extension attempts to open a new tab via
@@ -311,8 +316,16 @@ interface TabHandler {
* @param engineSession an instance of engine session to open a new tab with.
* @param active whether or not the new tab should be active/selected.
* @param url the target url to be loaded in a new tab.
- */
- fun onNewTab(webExtension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) = Unit
+ * @param isPrivate whether private browsing mode is enabled for the new
+ * tab. Must match the engineSession.privateMode flag.
+ */
+ fun onNewTab(
+ webExtension: WebExtension,
+ engineSession: EngineSession,
+ active: Boolean,
+ url: String,
+ isPrivate: Boolean,
+ ) = Unit
/**
* Invoked when a web extension attempts to update a tab via
diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt
index 86eeb61a442..11827ac028e 100644
--- a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt
+++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtensionDelegate.kt
@@ -62,6 +62,12 @@ interface WebExtensionDelegate {
*/
fun onAllowedInPrivateBrowsingChanged(extension: WebExtension) = Unit
+ /**
+ * Invoked to determine the current private browsing mode. New tabs opened
+ * by extensions may use this state if not specified otherwise.
+ */
+ fun isInPrivateBrowsing(): Boolean = false
+
/**
* Invoked when a web extension attempts to open a new tab via
* browser.tabs.create. Note that browser.tabs.update and browser.tabs.remove
@@ -72,8 +78,16 @@ interface WebExtensionDelegate {
* @param engineSession an instance of engine session to open a new tab with.
* @param active whether or not the new tab should be active/selected.
* @param url the target url to be loaded in a new tab.
+ * @param isPrivate whether private browsing mode is enabled for the new
+ * tab. Must match the engineSession.privateMode flag.
*/
- fun onNewTab(extension: WebExtension, engineSession: EngineSession, active: Boolean, url: String) = Unit
+ fun onNewTab(
+ extension: WebExtension,
+ engineSession: EngineSession,
+ active: Boolean,
+ url: String,
+ isPrivate: Boolean,
+ ) = Unit
/**
* Invoked when a web extension defines a browser action. To listen for session-specific
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
index 376a205c8d1..19f680f72b8 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt
@@ -1972,7 +1972,7 @@ class GeckoEngineTest {
tabDelegateCaptor.value.onNewTab(extension, createTabDetails)
val extensionCaptor = argumentCaptor<WebExtension>()
- verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""))
+ verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""), eq(false))
assertEquals(extId, extensionCaptor.value.id)
}
@@ -2076,7 +2076,7 @@ class GeckoEngineTest {
tabDelegateCaptor.value.onNewTab(extension, createTabDetails)
val extensionCaptor = argumentCaptor<WebExtension>()
- verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""))
+ verify(webExtensionsDelegate).onNewTab(extensionCaptor.capture(), any(), eq(false), eq(""), eq(false))
assertEquals(extId, extensionCaptor.value.id)
}
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
index 0115b874a8d..cd4f8f3731e 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt
@@ -340,13 +340,54 @@ class GeckoWebExtensionTest {
// Verify that tab methods are forwarded to the handler
val tabDetails = mockCreateTabDetails(active = true, url = "url")
tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
- verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"))
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"), eq(false))
assertNotNull(engineSessionCaptor.value)
+ assertFalse(engineSessionCaptor.value.geckoSession.settings.usePrivateMode)
tabDelegateCaptor.value.onOpenOptionsPage(nativeGeckoWebExt)
verify(tabHandler).onOpenOptionsPage(eq(extension))
}
+ @Test
+ fun `global tab handler opens a private tab when private browsing mode is active`() {
+ val runtime: GeckoRuntime = mock()
+ whenever(runtime.settings).thenReturn(mock())
+ whenever(runtime.webExtensionController).thenReturn(mock())
+ val tabHandler: TabHandler = mock()
+ val tabDelegateCaptor = argumentCaptor<WebExtension.TabDelegate>()
+ val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
+
+ val nativeGeckoWebExt: WebExtension =
+ mockNativeWebExtension(id = "id", location = "uri", metaData = mockNativeWebExtensionMetaData())
+
+ // Create extension and register global tab handler
+ val extension = GeckoWebExtension(
+ runtime = runtime,
+ nativeExtension = nativeGeckoWebExt,
+ )
+ val defaultSettings: DefaultSettings = mock()
+
+ // Simulate that the user has enabled private browsing.
+ whenever(tabHandler.isInPrivateBrowsing()).thenReturn(true)
+
+ extension.registerTabHandler(tabHandler, defaultSettings)
+ verify(nativeGeckoWebExt).tabDelegate = tabDelegateCaptor.capture()
+
+ // Simulate browser.tabs.create() call from extension.
+ val tabDetails = mockCreateTabDetails(active = true, url = "url")
+ tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"), eq(true))
+ assertTrue(engineSessionCaptor.value.geckoSession.settings.usePrivateMode)
+
+ // Simulate that the user switched back to non-private browsing.
+ whenever(tabHandler.isInPrivateBrowsing()).thenReturn(false)
+
+ // Now browser.tabs.create() should open in a non-private browsing context.
+ tabDelegateCaptor.value.onNewTab(nativeGeckoWebExt, tabDetails)
+ verify(tabHandler).onNewTab(eq(extension), engineSessionCaptor.capture(), eq(true), eq("url"), eq(false))
+ assertFalse(engineSessionCaptor.value.geckoSession.settings.usePrivateMode)
+ }
+
@Test
fun `register session-specific tab handler`() {
val runtime: GeckoRuntime = mock()
diff --git a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt
index 39e9e766ebc..50ebd89e4b1 100644
--- a/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt
+++ b/mobile/android/android-components/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt
@@ -31,7 +31,7 @@ class WebExtensionTest {
tabHandler.onUpdateTab(mock(), mock(), false, "")
tabHandler.onCloseTab(mock(), mock())
- tabHandler.onNewTab(mock(), mock(), false, "")
+ tabHandler.onNewTab(mock(), mock(), false, "", false)
}
@Test
diff --git a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt
index e31dd34ee26..8fc701e7efe 100644
--- a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt
+++ b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt
@@ -146,7 +146,7 @@ class WebExtensionSupportTest {
WebExtensionSupport.initialize(engine, store)
verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
- delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
+ delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org", false)
captureMiddleware.assertFirstAction(TabListAction.AddTabAction::class) { action ->
assertEquals("https://mozilla.org", action.tab.content.url)
@@ -169,14 +169,14 @@ class WebExtensionSupportTest {
WebExtensionSupport.initialize(
engine,
store,
- onNewTabOverride = { _, _, _, _ ->
+ onNewTabOverride = { _, _, _, _, _ ->
onNewTabCalled = true
"123"
},
)
verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
- delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
+ delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org", false)
assertTrue(onNewTabCalled)
}
@@ -193,7 +193,7 @@ class WebExtensionSupportTest {
WebExtensionSupport.initialize(
engine,
store,
- onNewTabOverride = { _, _, _, selected ->
+ onNewTabOverride = { _, _, _, selected, _ ->
capturedSelected.add(selected)
"session-${capturedSelected.size}"
},
@@ -203,8 +203,8 @@ class WebExtensionSupportTest {
)
verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
- delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
- delegateCaptor.value.onNewTab(ext, engineSession, false, "https://mozilla.org")
+ delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org", false)
+ delegateCaptor.value.onNewTab(ext, engineSession, false, "https://mozilla.org", false)
assertEquals(listOf(true, false), capturedSelected)
// onSelectTabOverride should only fire for the active=true case; the override
@@ -223,7 +223,7 @@ class WebExtensionSupportTest {
WebExtensionSupport.initialize(engine, store)
verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
- delegateCaptor.value.onNewTab(ext, engineSession, false, "https://mozilla.org")
+ delegateCaptor.value.onNewTab(ext, engineSession, false, "https://mozilla.org", false)
captureMiddleware.assertFirstAction(TabListAction.AddTabAction::class) { action ->
assertEquals("https://mozilla.org", action.tab.content.url)
Loading diff…
References
On This Page