Firefox · Core
CVE-2025-6435
Logic Error in Core
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
registerCleanupFunctiondevtools/client/netmonitor/test/browser_net_save_response_as.js |
modified | |
add_taskdevtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js |
modified | |
registerCleanupFunctiondevtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js |
modified |
Files Changed
devtools/client/netmonitor/test/browser.tomldevtools/client/netmonitor/test/browser_net_save_response_as.jsdevtools/client/netmonitor/test/browser_net_save_response_as_sanitization.jsdevtools/client/netmonitor/test/head.jsdevtools/shared/DevToolsUtils.js
Patch
diff --git a/devtools/client/netmonitor/test/browser.toml b/devtools/client/netmonitor/test/browser.toml
index 8e9d4a6f9d4..c8679d810d2 100644
--- a/devtools/client/netmonitor/test/browser.toml
+++ b/devtools/client/netmonitor/test/browser.toml
@@ -436,6 +436,8 @@ fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and
["browser_net_save_response_as.js"]
+["browser_net_save_response_as_sanitization.js"]
+
["browser_net_script_cache.js"]
["browser_net_search-results.js"]
diff --git a/devtools/client/netmonitor/test/browser_net_save_response_as.js b/devtools/client/netmonitor/test/browser_net_save_response_as.js
index 638fb50794c..83600463ddf 100644
--- a/devtools/client/netmonitor/test/browser_net_save_response_as.js
+++ b/devtools/client/netmonitor/test/browser_net_save_response_as.js
@@ -41,26 +41,15 @@ add_task(async function () {
registerCleanupFunction(function () {
MockFilePicker.cleanup();
- destDir.remove(true);
});
// Select gzip request.
- info("Open the context menu");
-
- EventUtils.sendMouseEvent(
- { type: "mousedown" },
- document.querySelectorAll(".request-list-item")[6]
- );
-
- EventUtils.sendMouseEvent(
- { type: "contextmenu" },
+ await triggerSaveResponseAs(
+ monitor,
document.querySelectorAll(".request-list-item")[6]
);
- info("Open the save dialog");
- await selectContextMenuItem(monitor, "request-list-context-save-response-as");
-
info("Wait for the save dialog to close");
const savedPath = await saveDialogClosedPromise;
@@ -69,14 +58,7 @@ add_task(async function () {
is(savedPath, expectedFile.path, "Response was saved to correct path");
- info("Wait for the downloaded file to be fully saved to disk: " + savedPath);
- await TestUtils.waitForCondition(async () => {
- if (!(await IOUtils.exists(savedPath))) {
- return false;
- }
- const { size } = await IOUtils.stat(savedPath);
- return size > 0;
- });
+ await waitForFileSavedToDisk(savedPath);
const buffer = await IOUtils.read(savedPath);
const savedFileContent = new TextDecoder().decode(buffer);
@@ -91,12 +73,3 @@ add_task(async function () {
await teardown(monitor);
});
-
-function createTemporarySaveDirectory() {
- const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
- saveDir.append("testsavedir");
- if (!saveDir.exists()) {
- saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
- }
- return saveDir;
-}
diff --git a/devtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js b/devtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js
new file mode 100644
index 00000000000..2e80f7f3549
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window.browsingContext);
+
+/**
+ * Tests that filenames are sanitized when using Save Response As
+ */
+
+function setupTestServer() {
+ const httpServer = createTestHTTPServer();
+ httpServer.registerContentType("html", "text/html");
+
+ httpServer.registerPathHandler("/index.html", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`<!DOCTYPE html>
+ <html><body><h1>Test sanitization for save response as
+ <script>fetch("test.url");</script>
+ <script>fetch("test2.url");</script>
+ `);
+ });
+
+ httpServer.registerPathHandler("/test.url", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("dummy content");
+ });
+ httpServer.registerPathHandler("/test2.url", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("dummy content 2");
+ });
+
+ return httpServer;
+}
+
+add_task(async function () {
+ const httpServer = setupTestServer();
+ const port = httpServer.identity.primaryPort;
+
+ const { monitor } = await initNetMonitor(
+ `http://localhost:${port}/index.html`,
+ {
+ requestCount: 3,
+ }
+ );
+
+ info("Starting test... ");
+ const { document } = monitor.panelWin;
+
+ info("Reload the browser to show the 2 requests for the page");
+ const networkEvent = waitForNetworkEvents(monitor, 3);
+ await reloadBrowser();
+ await networkEvent;
+
+ // Create the folder the gzip file will be saved into
+ const destDir = createTemporarySaveDirectory();
+ let destFile;
+
+ // Prepare the MockFilePicker
+ MockFilePicker.displayDirectory = destDir;
+ registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+ });
+
+ info("Prepare a file picker mock which will use the default filename");
+ let saveDialogClosedPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ info("MockFilePicker showCallback - preserve .download extension");
+ const fileName = fp.defaultString;
+ destFile = destDir.clone();
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+
+ resolve(destFile.path);
+ };
+ });
+
+ info("Save response as for the test.url request");
+ await triggerSaveResponseAs(
+ monitor,
+ document.querySelectorAll(".request-list-item")[1]
+ );
+
+ info("Wait for the save dialog to close");
+ const filePickerPath = await saveDialogClosedPromise;
+
+ const expectedFile = destDir.clone();
+ expectedFile.append("test.url.download");
+ is(
+ filePickerPath,
+ expectedFile.path,
+ "File picker default filename was set to the expected value"
+ );
+
+ await waitForFileSavedToDisk(expectedFile.path);
+
+ info("Prepare a file picker mock which will override the default filename");
+ saveDialogClosedPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ info("MockFilePicker showCallback - strip .download extension");
+ const fileName = fp.defaultString;
+ destFile = destDir.clone();
+ destFile.append(fileName.replace(".download", ""));
+ MockFilePicker.setFiles([destFile]);
+
+ resolve(destFile.path);
+ };
+ });
+
+ info("Save response as for test2.url");
+ await triggerSaveResponseAs(
+ monitor,
+ document.querySelectorAll(".request-list-item")[2]
+ );
+
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/devtools/client/netmonitor/test/browser.toml b/devtools/client/netmonitor/test/browser.toml
index 8e9d4a6f9d4..c8679d810d2 100644
--- a/devtools/client/netmonitor/test/browser.toml
+++ b/devtools/client/netmonitor/test/browser.toml
@@ -436,6 +436,8 @@ fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and
["browser_net_save_response_as.js"]
+["browser_net_save_response_as_sanitization.js"]
+
["browser_net_script_cache.js"]
["browser_net_search-results.js"]
diff --git a/devtools/client/netmonitor/test/browser_net_save_response_as.js b/devtools/client/netmonitor/test/browser_net_save_response_as.js
index 638fb50794c..83600463ddf 100644
--- a/devtools/client/netmonitor/test/browser_net_save_response_as.js
+++ b/devtools/client/netmonitor/test/browser_net_save_response_as.js
@@ -41,26 +41,15 @@ add_task(async function () {
registerCleanupFunction(function () {
MockFilePicker.cleanup();
- destDir.remove(true);
});
// Select gzip request.
- info("Open the context menu");
-
- EventUtils.sendMouseEvent(
- { type: "mousedown" },
- document.querySelectorAll(".request-list-item")[6]
- );
-
- EventUtils.sendMouseEvent(
- { type: "contextmenu" },
+ await triggerSaveResponseAs(
+ monitor,
document.querySelectorAll(".request-list-item")[6]
);
- info("Open the save dialog");
- await selectContextMenuItem(monitor, "request-list-context-save-response-as");
-
info("Wait for the save dialog to close");
const savedPath = await saveDialogClosedPromise;
@@ -69,14 +58,7 @@ add_task(async function () {
is(savedPath, expectedFile.path, "Response was saved to correct path");
- info("Wait for the downloaded file to be fully saved to disk: " + savedPath);
- await TestUtils.waitForCondition(async () => {
- if (!(await IOUtils.exists(savedPath))) {
- return false;
- }
- const { size } = await IOUtils.stat(savedPath);
- return size > 0;
- });
+ await waitForFileSavedToDisk(savedPath);
const buffer = await IOUtils.read(savedPath);
const savedFileContent = new TextDecoder().decode(buffer);
@@ -91,12 +73,3 @@ add_task(async function () {
await teardown(monitor);
});
-
-function createTemporarySaveDirectory() {
- const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
- saveDir.append("testsavedir");
- if (!saveDir.exists()) {
- saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
- }
- return saveDir;
-}
diff --git a/devtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js b/devtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js
new file mode 100644
index 00000000000..2e80f7f3549
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_save_response_as_sanitization.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window.browsingContext);
+
+/**
+ * Tests that filenames are sanitized when using Save Response As
+ */
+
+function setupTestServer() {
+ const httpServer = createTestHTTPServer();
+ httpServer.registerContentType("html", "text/html");
+
+ httpServer.registerPathHandler("/index.html", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`<!DOCTYPE html>
+ <html><body><h1>Test sanitization for save response as
+ <script>fetch("test.url");</script>
+ <script>fetch("test2.url");</script>
+ `);
+ });
+
+ httpServer.registerPathHandler("/test.url", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("dummy content");
+ });
+ httpServer.registerPathHandler("/test2.url", function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("dummy content 2");
+ });
+
+ return httpServer;
+}
+
+add_task(async function () {
+ const httpServer = setupTestServer();
+ const port = httpServer.identity.primaryPort;
+
+ const { monitor } = await initNetMonitor(
+ `http://localhost:${port}/index.html`,
+ {
+ requestCount: 3,
+ }
+ );
+
+ info("Starting test... ");
+ const { document } = monitor.panelWin;
+
+ info("Reload the browser to show the 2 requests for the page");
+ const networkEvent = waitForNetworkEvents(monitor, 3);
+ await reloadBrowser();
+ await networkEvent;
+
+ // Create the folder the gzip file will be saved into
+ const destDir = createTemporarySaveDirectory();
+ let destFile;
+
+ // Prepare the MockFilePicker
+ MockFilePicker.displayDirectory = destDir;
+ registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+ });
+
+ info("Prepare a file picker mock which will use the default filename");
+ let saveDialogClosedPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ info("MockFilePicker showCallback - preserve .download extension");
+ const fileName = fp.defaultString;
+ destFile = destDir.clone();
+ destFile.append(fileName);
+ MockFilePicker.setFiles([destFile]);
+
+ resolve(destFile.path);
+ };
+ });
+
+ info("Save response as for the test.url request");
+ await triggerSaveResponseAs(
+ monitor,
+ document.querySelectorAll(".request-list-item")[1]
+ );
+
+ info("Wait for the save dialog to close");
+ const filePickerPath = await saveDialogClosedPromise;
+
+ const expectedFile = destDir.clone();
+ expectedFile.append("test.url.download");
+ is(
+ filePickerPath,
+ expectedFile.path,
+ "File picker default filename was set to the expected value"
+ );
+
+ await waitForFileSavedToDisk(expectedFile.path);
+
+ info("Prepare a file picker mock which will override the default filename");
+ saveDialogClosedPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ info("MockFilePicker showCallback - strip .download extension");
+ const fileName = fp.defaultString;
+ destFile = destDir.clone();
+ destFile.append(fileName.replace(".download", ""));
+ MockFilePicker.setFiles([destFile]);
+
+ resolve(destFile.path);
+ };
+ });
+
+ info("Save response as for test2.url");
+ await triggerSaveResponseAs(
+ monitor,
+ document.querySelectorAll(".request-list-item")[2]
+ );
+
+ info("Wait for the save dialog to close");
+ const updatedFilePickerPath = await saveDialogClosedPromise;
+
+ const invalidFile = destDir.clone();
+ invalidFile.append("test2.url");
+
+ const expectedFile2 = destDir.clone();
+ expectedFile2.append("test2.url.download");
+
+ is(
+ updatedFilePickerPath,
+ invalidFile.path,
+ "File picker filename was updated to an invalid path while saving"
+ );
+
+ // Check that the valid path was still used to save the file.
+ await waitForFileSavedToDisk(expectedFile2.path);
+
+ ok(
+ !(await IOUtils.exists(invalidFile.path)),
+ "No file was saved for the invalid path"
+ );
+
+ await teardown(monitor);
+});
diff --git a/devtools/client/netmonitor/test/head.js b/devtools/client/netmonitor/test/head.js
index d332cca066c..648000ffd6f 100644
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -1788,3 +1788,60 @@ function findRequestByInitiator(document, initiator) {
}
return null;
}
+
+/**
+ * Click on the "save response as" context menu item for the provided request
+ * element in the provided netmonitor panel.
+ *
+ * Resolves when the context menu is closed.
+ *
+ * @param {object} monitor
+ * The netmonitor instance
+ * @param {HTMLElement} request
+ * The request item in the netmonitor table
+ */
+async function triggerSaveResponseAs(monitor, request) {
+ EventUtils.sendMouseEvent({ type: "mousedown" }, request);
+ EventUtils.sendMouseEvent({ type: "contextmenu" }, request);
+
+ info("Open the save dialog");
+ await selectContextMenuItem(monitor, "request-list-context-save-response-as");
+}
+
+/**
+ * Wait until the provided path has a non-zero size on the file system.
+ *
+ * @param {string} path
+ * The path to wait for.
+ */
+async function waitForFileSavedToDisk(path) {
+ info("Wait for the downloaded file to be fully saved to disk: " + path);
+ await TestUtils.waitForCondition(async () => {
+ if (!(await IOUtils.exists(path))) {
+ return false;
+ }
+ const { size } = await IOUtils.stat(path);
+ return size > 0;
+ });
+}
+
+/**
+ * Create a temporary directory to save files for a test.
+ * Register a cleanup function to delete the directory after the test.
+ *
+ * @returns {nsIFile}
+ * The created temporary directory.
+ */
+function createTemporarySaveDirectory() {
+ const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+
+ registerCleanupFunction(function () {
+ saveDir.remove(true);
+ });
+ return saveDir;
+}
Loading diff…
References
On This Page