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

Background

Data Loss Prevention (DLP) / Content Analysis
the nsIContentAnalysis mechanism that submits downloaded file contents to an external agent to allow or block the download.
Part file
the temporary .part file that a download is written to (download.target.partFilePath) before it is moved to its final destination (download.target.path).
`DownloadIntegration`
the toolkit/components/downloads/DownloadIntegration.sys.mjs module that builds the content-analysis request for a completed download.
`filePath`
the field in the content-analysis request that tells the scanner which on-disk file to inspect.

Root Cause Analysis

When a download completes, DownloadIntegration constructs a content-analysis request and populates its filePath from download.target.path, the final destination path. However, at the point the scan is requested the downloaded bytes still live in the .part file at download.target.partFilePath and have not yet been moved to download.target.path, so when a part file is in use the filePath pointed at a path that did not yet hold the download’s contents. This violated the invariant that the file handed to Content Analysis must be the actual downloaded data being evaluated, causing the DLP scan to inspect a missing or wrong file and effectively pass without seeing the real payload.

The fix uses download.target.partFilePath || download.target.path, so the scanner receives the part-file path that actually contains the downloaded bytes whenever a part file is involved, and falls back to the final path only when no part file exists.

The added test hook in head.js now asserts the requested filePath exists on disk, and the browser tests exercise both the part-file and non-part-file cases and compare scanned bytes against the download.

Key insight
The single mistake was pointing Content Analysis at the not-yet-populated final download.target.path instead of the download.target.partFilePath that actually held the downloaded data; the fix restores correct scanning by preferring partFilePath when a part file is present.

Attack Path

  1. Craft a policy-violating download An attacker or insider serves a file whose contents would be blocked by the organization’s DLP policy.
  2. Trigger a part-file download The download proceeds through a configuration that writes to a .part file, so download.target.partFilePath is set and distinct from download.target.path.
  3. DLP scans the wrong path DownloadIntegration submits download.target.path as filePath, but the real bytes are in the .part file, so the scanner sees no matching content.
  4. Scan passes and file lands Content Analysis returns allow, the .part file is moved to the final path, and the disallowed file is delivered despite the enterprise policy.

Impact Assessment

An attacker gains a mitigation bypass of the Data Loss Prevention control in the parent process, allowing a file that should be blocked (or exfiltrated data on inbound scan) to complete without being inspected. The precondition is an environment with Content Analysis / DLP enabled and a download that uses a .part intermediate file, which is the common download path. Impact is rated low because it requires DLP to be deployed and only defeats an organizational policy rather than breaching browser security boundaries.

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
registerCleanupFunction
toolkit/components/contentanalysis/tests/browser/browser_download_save_link_as_content_analysis.js
modified
if
toolkit/components/contentanalysis/tests/browser/head.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

Audit Directions

  • Path vs. data timing
    Audit any code that hands a file path to a security scanner or validator and confirm the path points at where the bytes currently reside, not at a destination the data has not yet been moved to.
  • Optional intermediate files
    Review uses of download.target.path across the downloads and content-analysis code for other places that ignore partFilePath, since the .part indirection can silently change which file is examined.
  • Test coverage of both branches
    Ensure security-relevant features have tests exercising both the part-file and no-part-file code paths and asserting the checked file exists, as the new head.js filePath existence check now enforces.
diff --git a/toolkit/components/downloads/DownloadIntegration.sys.mjs b/toolkit/components/downloads/DownloadIntegration.sys.mjs
index 038472a1c33..4fc5e7a245c 100644
--- a/toolkit/components/downloads/DownloadIntegration.sys.mjs
+++ b/toolkit/components/downloads/DownloadIntegration.sys.mjs
@@ -622,7 +622,9 @@ export var DownloadIntegration = {
               resources,
               requestToken,
               userActionId,
-              filePath: download.target.path,
+              // Use the path to the part-file if a part file is involved.
+              // We haven't moved it yet.
+              filePath: download.target.partFilePath || download.target.path,
               sha256Digest: download.saver.getSha256Hash(),
             }
           ),
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);
 
   await SpecialPowers.popPrefEnv();
 });
diff --git a/toolkit/components/contentanalysis/tests/browser/head.js b/toolkit/components/contentanalysis/tests/browser/head.js
index 2356dde6763..f5da5a14d55 100644
--- a/toolkit/components/contentanalysis/tests/browser/head.js
+++ b/toolkit/components/contentanalysis/tests/browser/head.js
@@ -277,6 +277,12 @@ function makeMockContentAnalysis() {
         Services.obs.notifyObservers(request, "dlp-request-made");
       }
 
+      // If we are checking a file, make sure it exists.
+      if (request.filePath) {
+        const nsiFile = new FileUtils.File(request.filePath);
+        ok(nsiFile.exists(), "File to check exists");
+      }
+
       // Use setTimeout to simulate an async activity.
       setTimeout(async () => {
         if (this.waitForEvent) {
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…