Firefox · Core
CVE-2026-118018
Logic Error in Core
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
StreamListenermobile/android/components/geckoview/GeckoViewExternalAppService.cpp |
modified | |
ifmobile/android/components/geckoview/GeckoViewExternalAppService.cpp |
modified |
Files Changed
mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.ktmobile/android/android-components/docs/changelog.mdmobile/android/components/geckoview/GeckoViewExternalAppService.cppmobile/android/components/geckoview/GeckoViewStreamListener.cppmobile/android/components/geckoview/GeckoViewStreamListener.hmobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.ktmobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
Patch
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
index 370f54f78e5..4d66c2f5018 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
@@ -68,7 +68,6 @@ import mozilla.components.support.ktx.kotlin.isEmail
import mozilla.components.support.ktx.kotlin.isExtensionUrl
import mozilla.components.support.ktx.kotlin.isGeoLocation
import mozilla.components.support.ktx.kotlin.isPhone
-import mozilla.components.support.ktx.kotlin.sanitizeFileName
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import mozilla.components.support.utils.CertificateUtils
import mozilla.components.support.utils.DownloadUtils
@@ -1500,7 +1499,7 @@ class GeckoEngineSession(
url = url,
contentLength = contentLength,
contentType = DownloadUtils.sanitizeMimeType(contentType),
- fileName = fileName?.sanitizeFileName(),
+ fileName = fileName,
response = response,
isPrivate = privateMode,
openInApp = webResponse.requestExternalApp,
diff --git a/mobile/android/android-components/docs/changelog.md b/mobile/android/android-components/docs/changelog.md
index b57fa4862c6..8a25a30ead2 100644
--- a/mobile/android/android-components/docs/changelog.md
+++ b/mobile/android/android-components/docs/changelog.md
@@ -7,6 +7,8 @@ permalink: /changelog/
# 160.0 (In Development)
* **ui-richtext**
* 🆕 Added `StreamingParser`, which parses a markdown document as it arrives in chunks, reparsing only the part of it that can still change. [Bug 2059255](https://bugzilla.mozilla.org/show_bug.cgi?id=2059255)
+* **browser-engine-gecko**
+ * Download filenames from `onExternalResponse` are now validated by Gecko with the same rules as desktop, so `GeckoEngineSession` no longer applies the `String.sanitizeFileName` helper from **support-ktx** to them. [Bug 1822968](https://bugzilla.mozilla.org/show_bug.cgi?id=1822968)
# 159.0
* **feature-readerview**
diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp
index cd614ddb25a..95a42d25bbc 100644
--- a/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp
+++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp
@@ -8,6 +8,9 @@
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "nsIChannel.h"
+#include "nsIMIMEService.h"
+#include "nsMimeTypes.h"
+#include "nsServiceManagerUtils.h"
#include "mozilla/widget/EventDispatcher.h"
#include "mozilla/widget/nsWindow.h"
@@ -17,16 +20,28 @@
class StreamListener final : public mozilla::GeckoViewStreamListener {
public:
- explicit StreamListener(nsWindow* aWindow)
- : GeckoViewStreamListener(), mWindow(aWindow) {
+ StreamListener(nsWindow* aWindow, const nsAString& aFileName)
+ : GeckoViewStreamListener(), mWindow(aWindow), mFileName(aFileName) {
MOZ_ASSERT(aWindow);
}
- void SendWebResponse(mozilla::java::WebResponse::Param aResponse) {
+ void AmendWebResponse(
+ mozilla::java::WebResponse::Builder::Param aBuilder) override {
+ if (mFileName.IsEmpty()) {
+ return;
+ }
+ nsAutoString disposition(u"attachment; filename=\""_ns);
+ disposition.Append(mFileName);
+ disposition.Append(u'"');
+ aBuilder->Header(mozilla::jni::StringParam(u"content-disposition"_ns),
+ mozilla::jni::StringParam(disposition));
+ }
+
+ void SendWebResponse(mozilla::java::WebResponse::Param aResponse) override {
mWindow->PassExternalResponse(aResponse);
}
- void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) {
+ void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) override {
// Currently we don't do anything about errors here
}
@@ -34,6 +49,8 @@ class StreamListener final : public mozilla::GeckoViewStreamListener {
private:
RefPtr<nsWindow> mWindow;
+ // Filename validated by the MIME service, or empty if none was found.
+ nsString mFileName;
};
mozilla::StaticRefPtr<GeckoViewExternalAppService>
@@ -82,7 +99,19 @@ NS_IMETHODIMP GeckoViewExternalAppService::CreateListener(
return NS_ERROR_ABORT;
}
- RefPtr<StreamListener> listener = new StreamListener(window);
+ nsAutoString fileName;
+ nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
+ if (mimeService) {
+ uint32_t flags = nsIMIMEService::VALIDATE_ALLOW_EMPTY;
+ if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
+ nsCaseInsensitiveCStringComparator)) {
+ flags |= nsIMIMEService::VALIDATE_GUESS_FROM_EXTENSION;
+ }
+ mimeService->GetValidFileName(aChannel, aMimeContentType, nullptr, flags,
+ fileName);
+ }
+
+ RefPtr<StreamListener> listener = new StreamListener(window, fileName);
nsresult rv;
rv = aChannel->SetNotificationCallbacks(listener);
diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.cpp b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp
index 2b5e3278e3a..78c0fdc7346 100644
--- a/mobile/android/components/geckoview/GeckoViewStreamListener.cpp
+++ b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp
@@ -260,6 +260,8 @@ nsresult GeckoViewStreamListener::HandleWebResponse(nsIRequest* aRequest) {
}
}
+ AmendWebResponse(builder);
+
java::WebResponse::GlobalRef response = builder->Build();
SendWebResponse(response);
diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.h b/mobile/android/components/geckoview/GeckoViewStreamListener.h
index d89350051a0..59f77dfa721 100644
--- a/mobile/android/components/geckoview/GeckoViewStreamListener.h
+++ b/mobile/android/components/geckoview/GeckoViewStreamListener.h
@@ -46,6 +46,9 @@ class GeckoViewStreamListener : public nsIStreamListener,
virtual nsresult HandleWebResponse(nsIRequest* aRequest);
+ // Called after all headers are set and before the response is built.
+ virtual void AmendWebResponse(java::WebResponse::Builder::Param aBuilder) {}
+
virtual void SendWebResponse(java::WebResponse::Param aResponse) = 0;
virtual void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) = 0;
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
index ca747e6f138..2d3e476f4ef 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -13,6 +13,14 @@ exclude: true
⚠️ breaking change and deprecation notices
+## v160
+- The `content-disposition` header of the [`WebResponse`][65.15] passed to
+ [`ContentDelegate.onExternalResponse`][160.1] now contains a filename validated with the same
+ rules desktop applies when saving a download, instead of the header sent by the server.
+ ([bug 1822968]({{bugzilla}}1822968))
+
+[160.1]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onExternalResponse(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.WebResponse)
+
## v157
- Added [`GeckoSession.getPdfViewerEditor`][157.1] and
[`PdfViewerController.SessionEditor.addSignature`][157.2] to place a signature on the PDF the
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
index c1109f5c98e..8dee5979ff8 100644
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -319,6 +319,38 @@ class ContentDelegateTest : BaseSessionTest() {
)
}
+ private fun assertDownloadFileName(contentDisposition: String, expected: String) {
+ mainSession.loadTestPath(withContentDisposition(HELLO_HTML_PATH, contentDisposition))
+
+ sessionRule.waitUntilCalled(
+ object : ContentDelegate {
+ @AssertCalled(count = 1)
+ override fun onExternalResponse(session: GeckoSession, response: WebResponse) {
+ assertThat(
+ "Content-Disposition filename is validated by Gecko",
+ response.headers["content-disposition"],
+ equalTo("attachment; filename=\"$expected\""),
+ )
+ }
+ }
+ )
+ }
+
+ @Test
+ fun downloadFileNameReplacesBidiOverride() {
+ assertDownloadFileName("attachment; filename*=UTF-8''file%E2%80%AEtxt.html", "file_txt.html")
+ }
+
+ @Test
+ fun downloadFileNameReplacesPathSeparators() {
+ assertDownloadFileName("attachment; filename=\"dir/sub\\\\name.html\"", "dir_sub_name.html")
+ }
+
+ @Test
+ fun downloadFileNameReplacesIllegalCharacters() {
+ assertDownloadFileName("attachment; filename=\"a:b*c?d.html\"", "a_b_c_d.html")
+ }
+
@IgnoreCrash
@Test
fun crashContent() {
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
index c1109f5c98e..8dee5979ff8 100644
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -319,6 +319,38 @@ class ContentDelegateTest : BaseSessionTest() {
)
}
+ private fun assertDownloadFileName(contentDisposition: String, expected: String) {
+ mainSession.loadTestPath(withContentDisposition(HELLO_HTML_PATH, contentDisposition))
+
+ sessionRule.waitUntilCalled(
+ object : ContentDelegate {
+ @AssertCalled(count = 1)
+ override fun onExternalResponse(session: GeckoSession, response: WebResponse) {
+ assertThat(
+ "Content-Disposition filename is validated by Gecko",
+ response.headers["content-disposition"],
+ equalTo("attachment; filename=\"$expected\""),
+ )
+ }
+ }
+ )
+ }
+
+ @Test
+ fun downloadFileNameReplacesBidiOverride() {
+ assertDownloadFileName("attachment; filename*=UTF-8''file%E2%80%AEtxt.html", "file_txt.html")
+ }
+
+ @Test
+ fun downloadFileNameReplacesPathSeparators() {
+ assertDownloadFileName("attachment; filename=\"dir/sub\\\\name.html\"", "dir_sub_name.html")
+ }
+
+ @Test
+ fun downloadFileNameReplacesIllegalCharacters() {
+ assertDownloadFileName("attachment; filename=\"a:b*c?d.html\"", "a_b_c_d.html")
+ }
+
@IgnoreCrash
@Test
fun crashContent() {
Loading diff…
References
On This Page