Commit cabc34dc authored by Olli Pettay's avatar Olli Pettay Committed by dsmith@mozilla.com
Browse files

Bug 2057204 - use the right AddSystemEventListener variant with...

Bug 2057204 - use the right AddSystemEventListener variant with nsFileControlFrame::mMouseListener,  a=diannaS

Original Revision: https://phabricator.services.mozilla.com/D314734

Differential Revision: https://phabricator.services.mozilla.com/D317322
parent b51a7711
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -34,6 +34,9 @@ support-files = [

["browser_form_post_from_file_to_http.js"]

["browser_input_file_untrusted_drop.js"]
support-files = ["file_input_file_untrusted_drop.html"]

["browser_refresh_after_document_write.js"]
support-files = ["file_refresh_after_document_write.html"]

+174 −0
Original line number Diff line number Diff line
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const TEST_PATH = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  "https://example.com"
);
const PAGE = TEST_PATH + "file_input_file_untrusted_drop.html";

const FILE_CONTENTS = "not for the page to see";

async function createDraggedFile() {
  const path = PathUtils.join(
    PathUtils.tempDir,
    "browser_input_file_untrusted_drop.txt"
  );
  await IOUtils.writeUTF8(path, FILE_CONTENTS);
  registerCleanupFunction(() => IOUtils.remove(path, { ignoreAbsent: true }));
  return File.createFromFileName(path);
}

function dragFileOverInput(browser, file) {
  return SpecialPowers.spawn(browser, [file], async draggedFile => {
    const doc = content.document;
    const input = doc.getElementById("fileinput");

    content.wrappedJSObject.installDragEnterStealer();

    const dragData = [[{ type: "application/x-moz-file", data: draggedFile }]];

    // EventUtils.startDragSession() is not available on this branch, so use
    // the drag service to start the session for a drag from another
    // application.
    const dragService = SpecialPowers.Cc[
      "@mozilla.org/widget/dragservice;1"
    ].getService(SpecialPowers.Ci.nsIDragService);
    dragService.startDragSessionForTests(
      content,
      SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_COPY
    );
    try {
      // Fires dragstart on #dragsource, then dragenter and dragover on the
      // file input. No drop.
      EventUtils.synthesizeDragOver(
        doc.getElementById("dragsource"),
        input,
        dragData,
        "copy",
        content
      );
    } finally {
      content.windowUtils.dragSession?.endDragSession(true);
    }

    const stolen = content.wrappedJSObject.stealResult;
    return {
      fileCountAfterDrag: input.files.length,
      stolen: stolen
        ? {
            types: Array.from(stolen.types),
            itemCount: stolen.itemCount,
            fileCountDuringDragEnter: stolen.fileCountDuringDragEnter,
            anyItemReadableAsFile: stolen.anyItemReadableAsFile,
            dropEventCancelled: stolen.dropEventCancelled,
            fileCountAfterUntrustedDrop: stolen.fileCountAfterUntrustedDrop,
          }
        : null,
    };
  });
}

add_task(async function test_stolen_datatransfer_in_untrusted_drop() {
  const file = await createDraggedFile();

  await BrowserTestUtils.withNewTab(PAGE, async browser => {
    const result = await dragFileOverInput(browser, file);

    ok(result.stolen, "The page saw a dragenter event");
    if (!result.stolen) {
      return;
    }

    ok(
      result.stolen.types.includes("Files"),
      "The page can see that the drag carries a file"
    );
    is(result.stolen.itemCount, 1, "The page can see one item");
    is(
      result.stolen.fileCountDuringDragEnter,
      0,
      "DataTransfer.files is empty during dragenter"
    );
    ok(
      !result.stolen.anyItemReadableAsFile,
      "getAsFile() returns null during dragenter"
    );

    ok(
      !result.stolen.dropEventCancelled,
      "The untrusted drop event was not handled by the file control"
    );
    is(
      result.stolen.fileCountAfterUntrustedDrop,
      0,
      "The untrusted drop did not set input.files"
    );
    is(result.fileCountAfterDrag, 0, "input.files is still empty");
  });
});

add_task(async function test_untrusted_events_with_page_made_datatransfer() {
  await BrowserTestUtils.withNewTab(PAGE, async browser => {
    const [dragOverAllowed, fileCount] = await SpecialPowers.spawn(
      browser,
      [],
      () => [
        content.wrappedJSObject.spoofUntrustedDragOver(),
        content.wrappedJSObject.spoofUntrustedDrop(),
      ]
    );

    ok(
      dragOverAllowed,
      "The file control did not preventDefault() an untrusted dragover"
    );
    is(fileCount, 0, "An untrusted drop did not set input.files");
  });
});

// Make sure the above isn't passing because dropping files stopped working.
add_task(async function test_trusted_drop_still_works() {
  const file = await createDraggedFile();

  await BrowserTestUtils.withNewTab(PAGE, async browser => {
    const result = await SpecialPowers.spawn(
      browser,
      [file],
      async draggedFile => {
        const doc = content.document;
        const input = doc.getElementById("fileinput");

        const changed = new Promise(resolve =>
          input.addEventListener("change", resolve, { once: true })
        );

        EventUtils.synthesizeDrop(
          doc.getElementById("dragsource"),
          input,
          [[{ type: "application/x-moz-file", data: draggedFile }]],
          "copy",
          content
        );

        // Only wait for the change event if the drop actually did something,
        // otherwise a regression here would hang until the test times out
        // instead of failing with a useful message.
        const fileCount = input.files.length;
        if (fileCount) {
          await changed;
        }

        return {
          fileCount,
          text: fileCount ? await input.files[0].text() : null,
        };
      }
    );

    is(result.fileCount, 1, "A trusted drop set input.files");
    is(result.text, FILE_CONTENTS, "The dropped file is readable");
  });
});
+77 −0
Original line number Diff line number Diff line
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Untrusted drop events on input type=file</title>
</head>
<body>
<div id="dragsource" draggable="true" style="width: 100px; height: 50px">drag me</div>
<input id="fileinput" type="file">
<script>
var stealResult = null;

function installDragEnterStealer() {
  window.addEventListener("dragenter", onDragEnter, true);
}

function onDragEnter(event) {
  const dt = event.dataTransfer;
  const input = document.getElementById("fileinput");

  let anyItemReadableAsFile = false;
  for (const item of dt.items) {
    if (item.getAsFile()) {
      anyItemReadableAsFile = true;
    }
  }

  stealResult = {
    types: Array.from(dt.types),
    itemCount: dt.items.length,
    fileCountDuringDragEnter: dt.files.length,
    anyItemReadableAsFile,
  };

  stealResult.dropEventCancelled = !input.dispatchEvent(
    new DragEvent("drop", {
      bubbles: true,
      cancelable: true,
      dataTransfer: dt,
    })
  );
  stealResult.fileCountAfterUntrustedDrop = input.files.length;
}

function spoofUntrustedDrop() {
  const input = document.getElementById("fileinput");
  input.dispatchEvent(
    new DragEvent("drop", {
      bubbles: true,
      cancelable: true,
      dataTransfer: makeFileDataTransfer(),
    })
  );
  return input.files.length;
}

function spoofUntrustedDragOver() {
  const input = document.getElementById("fileinput");
  // Returns false if something called preventDefault(), which is how the file
  // control signals that it would accept the drag.
  return input.dispatchEvent(
    new DragEvent("dragover", {
      bubbles: true,
      cancelable: true,
      dataTransfer: makeFileDataTransfer(),
    })
  );
}

function makeFileDataTransfer() {
  const dt = new DataTransfer();
  dt.items.add(new File(["spoofed"], "spoofed.txt", { type: "text/plain" }));
  return dt;
}
</script>
</body>
</html>
+3 −2
Original line number Diff line number Diff line
@@ -140,8 +140,9 @@ nsresult nsFileControlFrame::CreateAnonymousContent(
  aElements.AppendElement(mTextContent);

  // We should be able to interact with the element by doing drag and drop.
  mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false);
  mContent->AddSystemEventListener(u"dragover"_ns, mMouseListener, false);
  mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false, false);
  mContent->AddSystemEventListener(u"dragover"_ns, mMouseListener, false,
                                   false);

  SyncDisabledState();