Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionMitigation bypass in the Data Loss Prevention component
ComponentToolkit
Bug ClassLogic Error
Tracker2051897
Fix commit6a0cabaad070 (firefox) +137/-35
CISA KEVNot listed
Credited5up3rh3i
Disclosed2026-08-18

Changed Functions

FunctionChangeNotes
add_task
toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
modified
if
toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
modified

Files Changed

  • toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
  • toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
  • toolkit/components/contentanalysis/tests/browser/head.js
  • toolkit/components/downloads/DownloadIntegration.sys.mjs
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
index 0b578d8a5b5..bad2fa0a975 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
@@ -12,9 +12,9 @@ add_setup(async function test_setup() {
 const DOWNLOAD_URL =
   "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/file_to_download.unknownextension";
 
-async function createTargetFileAndDownload() {
+async function createTargetFileAndDownload(usePartFile) {
   // Create a temporary file that will be downloaded to.
-  const targetFile = await IOUtils.createUniqueFile(
+  const targetFileName = await IOUtils.createUniqueFile(
     PathUtils.tempDir,
     "target_download_for_content_analysis.txt",
     0o600
@@ -23,13 +23,16 @@ async function createTargetFileAndDownload() {
   // successfully downloaded. Note that this theoretically introduces
   // a race condition since something else could come in and create
   // a temp file with the same name. This seems unlikely.
-  await IOUtils.remove(targetFile, { ignoreAbsent: false });
+  await IOUtils.remove(targetFileName, { ignoreAbsent: false });
 
   return await Downloads.createDownload({
     source: {
       url: DOWNLOAD_URL,
     },
-    target: targetFile,
+    target: {
+      path: targetFileName,
+      partFilePath: usePartFile ? targetFileName + ".part" : undefined,
+    },
   });
 }
 
@@ -90,7 +93,7 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
   ok(!!request.requestToken.length, "request requestToken should not be empty");
 }
 
-add_task(async function test_download_content_analysis_allows() {
+async function check_download_content_analysis_allows(usePartFile) {
   mockCA.setupForTest(true);
   await SpecialPowers.pushPrefEnv({
     set: [
@@ -98,7 +101,7 @@ add_task(async function test_download_content_analysis_allows() {
     ],
   });
 
-  const download = await createTargetFileAndDownload();
+  const download = await createTargetFileAndDownload(usePartFile);
   let list = await Downloads.getList(Downloads.PUBLIC);
   await list.add(download);
   await download.start();
@@ -106,13 +109,33 @@ add_task(async function test_download_content_analysis_allows() {
   // Make sure the download succeeded.
   ok(download.succeeded, "Download should succeed");
   is(mockCA.calls.length, 1, "Content analysis should be called once");
-  assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
-  ok(await IOUtils.exists(download.target.path), "Target file should exist");
+  assertContentAnalysisDownloadRequest(
+    mockCA.calls[0],
+    usePartFile ? download.target.partFilePath : download.target.path
+  );
+  ok(
+    await IOUtils.exists(download.target.path),
+    "Final target file should exist"
+  );
+  if (usePartFile) {
+    ok(
+      !(await IOUtils.exists(download.target.partFilePath)),
+      "Part file should not still exist"
+    );
+  }
   await IOUtils.remove(download.target.path);
   await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_allows() {
+  await check_download_content_analysis_allows(false /* usePartFile */);
 });
 
-add_task(async function test_download_content_analysis_blocks() {
+add_task(async function test_download_content_analysis_allows_partfile() {
+  await check_download_content_analysis_allows(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_blocks(usePartFile) {
   mockCA.setupForTest(false);
   await SpecialPowers.pushPrefEnv({
     set: [
@@ -120,28 +143,45 @@ add_task(async function test_download_content_analysis_blocks() {
     ],
   });
 
-  const download = await createTargetFileAndDownload();
-  info(`path is ${download.target.path}`);
+  const download = await createTargetFileAndDownload(usePartFile);
+  if (usePartFile) {
+    info(`part file path is ${download.target.partFilePath}`);
+  }
+  info(`final path is ${download.target.path}`);
   await Assert.rejects(
     download.start(),
     ex => ex instanceof Downloads.Error && ex.becauseBlockedByContentAnalysis,
     "Download should have been rejected"
   );
-  info(`path is ${download.target.path}`);
   ok(!download.succeeded, "Download should not succeed");
   is(mockCA.calls.length, 1, "Content analysis should be called once");
-  info(`path is ${download.target.path}`);
-  assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
-  info(`path is ${download.target.path}`);
+  assertContentAnalysisDownloadRequest(
+    mockCA.calls[0],
+    usePartFile ? download.target.partFilePath : download.target.path
+  );
 
   ok(
     !(await IOUtils.exists(download.target.path)),
-    "Target file should not exist"
+    "Final target file should not exist"
   );
+  if (usePartFile) {
+    ok(
+      !(await IOUtils.exists(download.target.partFilePath)),
+      "Part file should not still exist"
+    );
+  }
   await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_blocks() {
+  await check_download_content_analysis_blocks(false /* usePartFile */);
 });
 
-add_task(async function test_download_content_analysis_user_cancels() {
+add_task(async function test_download_content_analysis_blocks_partfile() {
+  await check_download_content_analysis_blocks(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_user_cancels(usePartFile) {
   // Make the mock CA service wait for event so the test can
   // cancel the download before the scan finishes.
   mockCA.setupForTest(true, /* waitForEvent */ true);
@@ -156,9 +196,13 @@ add_task(async function test_download_content_analysis_user_cancels() {
       once: true,
     });
   });
-  const download = await createTargetFileAndDownload();
+  const download = await createTargetFileAndDownload(usePartFile);
+
+  if (usePartFile) {
+    info(`part file path is ${download.target.partFilePath}`);
+  }
+  info(`final path is ${download.target.path}`);
 
-  info(`path is ${download.target.path}`);
   let list = await Downloads.getList(Downloads.PUBLIC);
   await list.add(download);
   download.start();
@@ -189,12 +233,20 @@ add_task(async function test_download_content_analysis_user_cancels() {
     // may or may not exist on disk.
   }
   await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_user_cancels() {
+  await check_download_content_analysis_user_cancels(false /* usePartFile */);
+});
+
+add_task(async function test_download_content_analysis_user_cancels_partfile() {
+  await check_download_content_analysis_user_cancels(true /* usePartFile */);
 });
 
 add_task(async function test_download_content_analysis_pref_defaults_to_off() {
-  mockCA.setupForTest(false);
   // do not set pref, so content analysis should not be consulted
-  const download = await createTargetFileAndDownload();
+  mockCA.setupForTest(false);
+  const download = await createTargetFileAndDownload(false);
   let list = await Downloads.getList(Downloads.PUBLIC);
   await list.add(download);
   await download.start();
@@ -202,6 +254,9 @@ add_task(async function test_download_content_analysis_pref_defaults_to_off() {
   // Make sure the download succeeded.
   ok(download.succeeded, "Download should succeed");
   is(mockCA.calls.length, 0, "Content analysis should not be called");
-  ok(await IOUtils.exists(download.target.path), "Target file should exist");
+  ok(
+    await IOUtils.exists(download.target.path),
+    "Final target file should exist"
+  );
   await IOUtils.remove(download.target.path);
 });
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
index 9ceef3b7088..4e9cd9815b3 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
@@ -23,27 +23,55 @@ async function triggerSaveAs({ selector }) {
   contextMenu.activateItem(saveLinkCommand);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
index 0b578d8a5b5..bad2fa0a975 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_content_analysis.js
@@ -12,9 +12,9 @@ add_setup(async function test_setup() {
 const DOWNLOAD_URL =
   "https://example.com/browser/toolkit/components/contentanalysis/tests/browser/file_to_download.unknownextension";
 
-async function createTargetFileAndDownload() {
+async function createTargetFileAndDownload(usePartFile) {
   // Create a temporary file that will be downloaded to.
-  const targetFile = await IOUtils.createUniqueFile(
+  const targetFileName = await IOUtils.createUniqueFile(
     PathUtils.tempDir,
     "target_download_for_content_analysis.txt",
     0o600
@@ -23,13 +23,16 @@ async function createTargetFileAndDownload() {
   // successfully downloaded. Note that this theoretically introduces
   // a race condition since something else could come in and create
   // a temp file with the same name. This seems unlikely.
-  await IOUtils.remove(targetFile, { ignoreAbsent: false });
+  await IOUtils.remove(targetFileName, { ignoreAbsent: false });
 
   return await Downloads.createDownload({
     source: {
       url: DOWNLOAD_URL,
     },
-    target: targetFile,
+    target: {
+      path: targetFileName,
+      partFilePath: usePartFile ? targetFileName + ".part" : undefined,
+    },
   });
 }
 
@@ -90,7 +93,7 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
   ok(!!request.requestToken.length, "request requestToken should not be empty");
 }
 
-add_task(async function test_download_content_analysis_allows() {
+async function check_download_content_analysis_allows(usePartFile) {
   mockCA.setupForTest(true);
   await SpecialPowers.pushPrefEnv({
     set: [
@@ -98,7 +101,7 @@ add_task(async function test_download_content_analysis_allows() {
     ],
   });
 
-  const download = await createTargetFileAndDownload();
+  const download = await createTargetFileAndDownload(usePartFile);
   let list = await Downloads.getList(Downloads.PUBLIC);
   await list.add(download);
   await download.start();
@@ -106,13 +109,33 @@ add_task(async function test_download_content_analysis_allows() {
   // Make sure the download succeeded.
   ok(download.succeeded, "Download should succeed");
   is(mockCA.calls.length, 1, "Content analysis should be called once");
-  assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
-  ok(await IOUtils.exists(download.target.path), "Target file should exist");
+  assertContentAnalysisDownloadRequest(
+    mockCA.calls[0],
+    usePartFile ? download.target.partFilePath : download.target.path
+  );
+  ok(
+    await IOUtils.exists(download.target.path),
+    "Final target file should exist"
+  );
+  if (usePartFile) {
+    ok(
+      !(await IOUtils.exists(download.target.partFilePath)),
+      "Part file should not still exist"
+    );
+  }
   await IOUtils.remove(download.target.path);
   await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_allows() {
+  await check_download_content_analysis_allows(false /* usePartFile */);
 });
 
-add_task(async function test_download_content_analysis_blocks() {
+add_task(async function test_download_content_analysis_allows_partfile() {
+  await check_download_content_analysis_allows(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_blocks(usePartFile) {
   mockCA.setupForTest(false);
   await SpecialPowers.pushPrefEnv({
     set: [
@@ -120,28 +143,45 @@ add_task(async function test_download_content_analysis_blocks() {
     ],
   });
 
-  const download = await createTargetFileAndDownload();
-  info(`path is ${download.target.path}`);
+  const download = await createTargetFileAndDownload(usePartFile);
+  if (usePartFile) {
+    info(`part file path is ${download.target.partFilePath}`);
+  }
+  info(`final path is ${download.target.path}`);
   await Assert.rejects(
     download.start(),
     ex => ex instanceof Downloads.Error && ex.becauseBlockedByContentAnalysis,
     "Download should have been rejected"
   );
-  info(`path is ${download.target.path}`);
   ok(!download.succeeded, "Download should not succeed");
   is(mockCA.calls.length, 1, "Content analysis should be called once");
-  info(`path is ${download.target.path}`);
-  assertContentAnalysisDownloadRequest(mockCA.calls[0], download.target.path);
-  info(`path is ${download.target.path}`);
+  assertContentAnalysisDownloadRequest(
+    mockCA.calls[0],
+    usePartFile ? download.target.partFilePath : download.target.path
+  );
 
   ok(
     !(await IOUtils.exists(download.target.path)),
-    "Target file should not exist"
+    "Final target file should not exist"
   );
+  if (usePartFile) {
+    ok(
+      !(await IOUtils.exists(download.target.partFilePath)),
+      "Part file should not still exist"
+    );
+  }
   await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_blocks() {
+  await check_download_content_analysis_blocks(false /* usePartFile */);
 });
 
-add_task(async function test_download_content_analysis_user_cancels() {
+add_task(async function test_download_content_analysis_blocks_partfile() {
+  await check_download_content_analysis_blocks(true /* usePartFile */);
+});
+
+async function check_download_content_analysis_user_cancels(usePartFile) {
   // Make the mock CA service wait for event so the test can
   // cancel the download before the scan finishes.
   mockCA.setupForTest(true, /* waitForEvent */ true);
@@ -156,9 +196,13 @@ add_task(async function test_download_content_analysis_user_cancels() {
       once: true,
     });
   });
-  const download = await createTargetFileAndDownload();
+  const download = await createTargetFileAndDownload(usePartFile);
+
+  if (usePartFile) {
+    info(`part file path is ${download.target.partFilePath}`);
+  }
+  info(`final path is ${download.target.path}`);
 
-  info(`path is ${download.target.path}`);
   let list = await Downloads.getList(Downloads.PUBLIC);
   await list.add(download);
   download.start();
@@ -189,12 +233,20 @@ add_task(async function test_download_content_analysis_user_cancels() {
     // may or may not exist on disk.
   }
   await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_download_content_analysis_user_cancels() {
+  await check_download_content_analysis_user_cancels(false /* usePartFile */);
+});
+
+add_task(async function test_download_content_analysis_user_cancels_partfile() {
+  await check_download_content_analysis_user_cancels(true /* usePartFile */);
 });
 
 add_task(async function test_download_content_analysis_pref_defaults_to_off() {
-  mockCA.setupForTest(false);
   // do not set pref, so content analysis should not be consulted
-  const download = await createTargetFileAndDownload();
+  mockCA.setupForTest(false);
+  const download = await createTargetFileAndDownload(false);
   let list = await Downloads.getList(Downloads.PUBLIC);
   await list.add(download);
   await download.start();
@@ -202,6 +254,9 @@ add_task(async function test_download_content_analysis_pref_defaults_to_off() {
   // Make sure the download succeeded.
   ok(download.succeeded, "Download should succeed");
   is(mockCA.calls.length, 0, "Content analysis should not be called");
-  ok(await IOUtils.exists(download.target.path), "Target file should exist");
+  ok(
+    await IOUtils.exists(download.target.path),
+    "Final target file should exist"
+  );
   await IOUtils.remove(download.target.path);
 });
diff --git a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
index 9ceef3b7088..4e9cd9815b3 100644
--- a/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
+++ b/toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
@@ -23,27 +23,55 @@ async function triggerSaveAs({ selector }) {
   contextMenu.activateItem(saveLinkCommand);
 }
 
+let tempDir;
+
+// Final file path for download.
 let destFilePath;
 
-add_setup(async () => {
-  mockCA = await mockContentAnalysisService(mockCA);
-});
+// path to file that is a copy of the file sent to CA, so we can (async) check
+// that it is the same file.
+let copiedScannedFilePath;
 
-function setupMockFilePicker() {
-  const tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+add_setup(async () => {
+  tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
   tempDir.append("test-download-dir");
   if (!tempDir.exists()) {
     tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
   }
+  registerCleanupFunction(function () {
+    if (tempDir.exists()) {
+      tempDir.remove(true);
+    }
+  });
 
+  let oldAnalyzeContentRequestPrivate = mockCA.analyzeContentRequestPrivate;
+  mockCA.analyzeContentRequestPrivate = function (
+    request,
+    autoAcknowledge,
+    callback
+  ) {
+    // Copy the file being scanned now because the original (a .part file) is
+    // renamed or removed once the download completes.
+    let requestFile = new FileUtils.File(request.filePath);
+    requestFile.copyTo(tempDir, "file.copy");
+    let tempDirClone = tempDir.clone();
+    tempDirClone.append("file.copy");
+    copiedScannedFilePath = tempDirClone.path;
+    oldAnalyzeContentRequestPrivate.call(
+      this,
+      request,
+      autoAcknowledge,
+      callback
+    );
+  };
+  mockCA = await mockContentAnalysisService(mockCA);
+});
+
+function setupMockFilePicker() {
   let MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init();
   registerCleanupFunction(function () {
     MockFilePicker.cleanup();
-
-    if (tempDir.exists()) {
-      tempDir.remove(true);
-    }
   });
 
   MockFilePicker.displayDirectory = tempDir;
@@ -59,7 +87,7 @@ function setupMockFilePicker() {
   };
 }
 
-function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
+async function assertContentAnalysisDownloadRequest(request) {
   is(request.url.spec, DOWNLOAD_URL, "request has correct URL");
   is(
     request.analysisType,
@@ -76,7 +104,17 @@ function assertContentAnalysisDownloadRequest(request, expectedFilePath) {
     Ci.nsIContentAnalysisRequest.eDownload,
     "request has correct operationTypeForDisplay"
   );
-  is(request.filePath, expectedFilePath, "request filePath should match");
+  // Compare file contents, not file names, as the download name will be the
+  // temporary name assigned before content analysis approval.
+  let scannedBytes = await IOUtils.read(copiedScannedFilePath);
+  let expectedBytes = new Uint8Array(
+    await (await fetch(DOWNLOAD_URL)).arrayBuffer()
+  );
+  Assert.deepEqual(
+    scannedBytes,
+    expectedBytes,
+    "scanned file is download file"
+  );
   ok(!request.textContent?.length, "request textContent should be empty");
   is(
     request.userActionRequestsCount,
@@ -109,8 +147,9 @@ add_task(async function test_download_content_analysis_download_save_as() {
     );
   }, "Wait for the file to be downloaded");
   is(mockCA.calls.length, 1, "Content analysis should be called once");
-  assertContentAnalysisDownloadRequest(mockCA.calls[0], destFilePath);
+  await assertContentAnalysisDownloadRequest(mockCA.calls[0]);
   await IOUtils.remove(destFilePath);
+  await IOUtils.remove(copiedScannedFilePath);
 
... (truncated)
Loading diff…