Bug 1777448 - Part 2: Suppress the paste context menu when the clipboard data originates from a same-origin page; r=nika

And don't allow new request being associated with existing pending request if
the requests are from different origin.

This patch also set proper principal to nsItransferable for clipboard write in
various cases,
- Copy image via context menu.
- Copy current selection via keyboard shortcut or context menu.
- Data is provided by script when copy operation is triggered via keyboard
  shortcut or context menu.
- Clipboard data is put via async clipboard.

Depends on D190761

Differential Revision: https://phabricator.services.mozilla.com/D190796
This commit is contained in:
Edgar Chen 2023-12-08 07:08:42 +00:00
Родитель 35aaa07f46
Коммит 1b518042e4
15 изменённых файлов: 536 добавлений и 76 удалений

Просмотреть файл

@ -80,7 +80,7 @@ static nsresult AppendDOMNode(nsITransferable* aTransferable,
// copy image as file promise onto the transferable
static nsresult AppendImagePromise(nsITransferable* aTransferable,
imgIRequest* aImgRequest,
nsIImageLoadingContent* aImageElement);
nsINode* aImageNode);
#endif
static nsresult EncodeForTextUnicode(nsIDocumentEncoder& aEncoder,
@ -244,6 +244,7 @@ static nsresult CreateTransferable(
NS_ENSURE_TRUE(aTransferable, NS_ERROR_NULL_POINTER);
aTransferable->Init(aDocument.GetLoadContext());
aTransferable->SetRequestingPrincipal(aDocument.NodePrincipal());
if (aEncodedDocumentWithContext.mUnicodeEncodingIsTextHTML) {
// Set up a format converter so that clipboard flavor queries work.
// This converter isn't really used for conversions.
@ -458,10 +459,14 @@ nsresult nsCopySupport::ImageCopy(nsIImageLoadingContent* aImageElement,
int32_t aCopyFlags) {
nsresult rv;
nsCOMPtr<nsINode> imageNode = do_QueryInterface(aImageElement, &rv);
NS_ENSURE_SUCCESS(rv, rv);
// create a transferable for putting data on the Clipboard
nsCOMPtr<nsITransferable> trans(do_CreateInstance(kCTransferableCID, &rv));
NS_ENSURE_SUCCESS(rv, rv);
trans->Init(aLoadContext);
trans->SetRequestingPrincipal(imageNode->NodePrincipal());
if (aCopyFlags & nsIDocumentViewerEdit::COPY_IMAGE_TEXT) {
// get the location from the element
@ -504,7 +509,7 @@ nsresult nsCopySupport::ImageCopy(nsIImageLoadingContent* aImageElement,
#ifdef XP_WIN
if (StaticPrefs::clipboard_imageAsFile_enabled()) {
rv = AppendImagePromise(trans, imgRequest, aImageElement);
rv = AppendImagePromise(trans, imgRequest, imageNode);
NS_ENSURE_SUCCESS(rv, rv);
}
#endif
@ -593,10 +598,10 @@ static nsresult AppendDOMNode(nsITransferable* aTransferable,
#ifdef XP_WIN
static nsresult AppendImagePromise(nsITransferable* aTransferable,
imgIRequest* aImgRequest,
nsIImageLoadingContent* aImageElement) {
nsINode* aImageNode) {
nsresult rv;
NS_ENSURE_TRUE(aImgRequest, NS_OK);
NS_ENSURE_TRUE(aImgRequest && aImageNode, NS_OK);
bool isMultipart;
rv = aImgRequest->GetMultipart(&isMultipart);
@ -605,9 +610,6 @@ static nsresult AppendImagePromise(nsITransferable* aTransferable,
return NS_OK;
}
nsCOMPtr<nsINode> node = do_QueryInterface(aImageElement, &rv);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
if (NS_WARN_IF(!mimeService)) {
return NS_ERROR_FAILURE;
@ -643,8 +645,8 @@ static nsresult AppendImagePromise(nsITransferable* aTransferable,
rv = AppendString(aTransferable, validFileName, kFilePromiseDestFilename);
NS_ENSURE_SUCCESS(rv, rv);
aTransferable->SetRequestingPrincipal(node->NodePrincipal());
aTransferable->SetCookieJarSettings(node->OwnerDoc()->CookieJarSettings());
aTransferable->SetCookieJarSettings(
aImageNode->OwnerDoc()->CookieJarSettings());
aTransferable->SetContentPolicyType(nsIContentPolicy::TYPE_INTERNAL_IMAGE);
// add the dataless file promise flavor

Просмотреть файл

@ -38,6 +38,7 @@
#include "mozilla/dom/DataTransferItemList.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/FileList.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/dom/BindingUtils.h"
@ -889,6 +890,17 @@ already_AddRefed<nsITransferable> DataTransfer::GetTransferable(
}
transferable->Init(aLoadContext);
// Set the principal of the global this DataTransfer was created for
// on the transferable for ReadWrite events (copy, cut, or dragstart).
//
// For other events, the data inside the transferable may originate
// from another origin or from the OS.
if (mMode == Mode::ReadWrite) {
if (nsCOMPtr<nsIGlobalObject> global = GetGlobal()) {
transferable->SetRequestingPrincipal(global->PrincipalOrNull());
}
}
nsCOMPtr<nsIStorageStream> storageStream;
nsCOMPtr<nsIObjectOutputStream> stream;
@ -1236,6 +1248,18 @@ void DataTransfer::GetRealFormat(const nsAString& aInFormat,
aOutFormat.Assign(lowercaseFormat);
}
already_AddRefed<nsIGlobalObject> DataTransfer::GetGlobal() const {
nsCOMPtr<nsIGlobalObject> global;
// This is annoying, but DataTransfer may have various things as parent.
if (nsCOMPtr<EventTarget> target = do_QueryInterface(mParent)) {
global = target->GetOwnerGlobal();
} else if (RefPtr<Event> event = do_QueryObject(mParent)) {
global = event->GetParentObject();
}
return global.forget();
}
nsresult DataTransfer::CacheExternalData(const char* aFormat, uint32_t aIndex,
nsIPrincipal* aPrincipal,
bool aHidden) {

Просмотреть файл

@ -426,6 +426,8 @@ class DataTransfer final : public nsISupports, public nsWrapperCache {
kImageRequestMime,
kPDFJSMime};
already_AddRefed<nsIGlobalObject> GetGlobal() const;
protected:
// caches text and uri-list data formats that exist in the drag service or
// clipboard for retrieval later.

Просмотреть файл

@ -15,7 +15,6 @@
#include "mozilla/dom/BlobImpl.h"
#include "mozilla/dom/DataTransferItemBinding.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/FileSystem.h"
#include "mozilla/dom/FileSystemDirectoryEntry.h"
#include "mozilla/dom/FileSystemFileEntry.h"
@ -302,7 +301,7 @@ already_AddRefed<File> DataTransferItem::GetAsFile(
if (RefPtr<Blob> blob = do_QueryObject(supports)) {
mCachedFile = blob->ToFile();
} else {
nsCOMPtr<nsIGlobalObject> global = GetGlobalFromDataTransfer();
nsCOMPtr<nsIGlobalObject> global = mDataTransfer->GetGlobal();
if (NS_WARN_IF(!global)) {
return nullptr;
}
@ -352,7 +351,7 @@ already_AddRefed<FileSystemEntry> DataTransferItem::GetAsEntry(
return nullptr;
}
nsCOMPtr<nsIGlobalObject> global = GetGlobalFromDataTransfer();
nsCOMPtr<nsIGlobalObject> global = mDataTransfer->GetGlobal();
if (NS_WARN_IF(!global)) {
return nullptr;
}
@ -428,7 +427,7 @@ already_AddRefed<File> DataTransferItem::CreateFileFromInputStream(
return nullptr;
}
nsCOMPtr<nsIGlobalObject> global = GetGlobalFromDataTransfer();
nsCOMPtr<nsIGlobalObject> global = mDataTransfer->GetGlobal();
if (NS_WARN_IF(!global)) {
return nullptr;
}
@ -491,18 +490,7 @@ void DataTransferItem::GetAsString(FunctionStringCallback* aCallback,
RefPtr<GASRunnable> runnable = new GASRunnable(aCallback, stringData);
// DataTransfer.mParent might be EventTarget, nsIGlobalObject, ClipboardEvent
// nsPIDOMWindowOuter, null
nsISupports* parent = mDataTransfer->GetParentObject();
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(parent);
if (parent && !global) {
if (nsCOMPtr<dom::EventTarget> target = do_QueryInterface(parent)) {
global = target->GetOwnerGlobal();
} else if (RefPtr<Event> event = do_QueryObject(parent)) {
global = event->GetParentObject();
}
}
if (global) {
if (nsCOMPtr<nsIGlobalObject> global = mDataTransfer->GetGlobal()) {
rv = global->Dispatch(runnable.forget());
} else {
rv = NS_DispatchToMainThread(runnable);
@ -594,22 +582,4 @@ already_AddRefed<nsIVariant> DataTransferItem::Data(nsIPrincipal* aPrincipal,
return variant.forget();
}
already_AddRefed<nsIGlobalObject>
DataTransferItem::GetGlobalFromDataTransfer() {
nsCOMPtr<nsIGlobalObject> global;
// This is annoying, but DataTransfer may have various things as parent.
nsCOMPtr<EventTarget> target =
do_QueryInterface(mDataTransfer->GetParentObject());
if (target) {
global = target->GetOwnerGlobal();
} else {
RefPtr<Event> event = do_QueryObject(mDataTransfer->GetParentObject());
if (event) {
global = event->GetParentObject();
}
}
return global.forget();
}
} // namespace mozilla::dom

Просмотреть файл

@ -115,8 +115,6 @@ class DataTransferItem final : public nsISupports, public nsWrapperCache {
nsIInputStream* aStream, const char* aFileNameKey,
const nsAString& aContentType);
already_AddRefed<nsIGlobalObject> GetGlobalFromDataTransfer();
// The index in the 2d mIndexedItems array
uint32_t mIndex;

Просмотреть файл

@ -12,6 +12,18 @@ skip-if = [
]
support-files = ["simple_navigator_clipboard_keydown.html"]
["browser_navigator_clipboard_contextmenu_suppression.js"]
support-files = [
"file_toplevel.html",
"file_iframe.html",
]
["browser_navigator_clipboard_contextmenu_suppression_ext.js"]
support-files = [
"file_toplevel.html",
"file_iframe.html",
]
["browser_navigator_clipboard_read.js"]
support-files = ["simple_navigator_clipboard_read.html"]
fail-if = ["a11y_checks"] # Bug 1854502 clicked browser may not be accessible

Просмотреть файл

@ -0,0 +1,264 @@
/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
requestLongerTimeout(2);
const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"https://example.com"
);
const kContentFileName = "file_toplevel.html";
const kContentFileUrl = kBaseUrlForContent + kContentFileName;
const kIsMac = navigator.platform.indexOf("Mac") > -1;
async function waitForPasteContextMenu() {
await waitForPasteMenuPopupEvent("shown");
let pasteButton = document.getElementById(kPasteMenuItemId);
info("Wait for paste button enabled");
await BrowserTestUtils.waitForMutationCondition(
pasteButton,
{ attributeFilter: ["disabled"] },
() => !pasteButton.disabled,
"Wait for paste button enabled"
);
}
async function readText(aBrowser) {
return SpecialPowers.spawn(aBrowser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
}
function testPasteContextMenuSuppression(aWriteFun, aMsg) {
add_task(async function test_context_menu_suppression_sameorigin() {
await BrowserTestUtils.withNewTab(
kContentFileUrl,
async function (browser) {
info(`Write data by ${aMsg}`);
let clipboardText = await aWriteFun(browser);
info("Test read from same-origin frame");
let listener = function (e) {
if (e.target.getAttribute("id") == kPasteMenuPopupId) {
ok(false, "paste contextmenu should not be shown");
}
};
document.addEventListener("popupshown", listener);
is(
await readText(browser.browsingContext.children[0]),
clipboardText,
"read should just be resolved without paste contextmenu shown"
);
document.removeEventListener("popupshown", listener);
}
);
});
add_task(async function test_context_menu_suppression_crossorigin() {
await BrowserTestUtils.withNewTab(
kContentFileUrl,
async function (browser) {
info(`Write data by ${aMsg}`);
let clipboardText = await aWriteFun(browser);
info("Test read from cross-origin frame");
let pasteButtonIsShown = waitForPasteContextMenu();
let readTextRequest = readText(browser.browsingContext.children[1]);
await pasteButtonIsShown;
info("Click paste button, request should be resolved");
await promiseClickPasteButton();
is(await readTextRequest, clipboardText, "Request should be resolved");
}
);
});
add_task(async function test_context_menu_suppression_multiple() {
await BrowserTestUtils.withNewTab(
kContentFileUrl,
async function (browser) {
info(`Write data by ${aMsg}`);
let clipboardText = await aWriteFun(browser);
info("Test read from cross-origin frame");
let pasteButtonIsShown = waitForPasteContextMenu();
let readTextRequest1 = readText(browser.browsingContext.children[1]);
await pasteButtonIsShown;
info(
"Test read from same-origin frame before paste contextmenu is closed"
);
is(
await readText(browser.browsingContext.children[0]),
clipboardText,
"read from same-origin should just be resolved without showing paste contextmenu shown"
);
info("Dismiss paste button, cross-origin request should be rejected");
await promiseDismissPasteButton();
await Assert.rejects(
readTextRequest1,
/NotAllowedError/,
"cross-origin request should be rejected"
);
}
);
});
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["dom.events.asyncClipboard.readText", true],
["dom.events.asyncClipboard.clipboardItem", true],
["test.events.async.enabled", true],
// Avoid paste button delay enabling making test too long.
["security.dialog_enable_delay", 0],
],
});
});
testPasteContextMenuSuppression(async aBrowser => {
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.writeText("${text}");`);
});
return clipboardText;
}, "clipboard.writeText()");
testPasteContextMenuSuppression(async aBrowser => {
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
content.document.notifyUserGestureActivation();
return content.eval(`
const itemInput = new ClipboardItem({["text/plain"]: "${text}"});
navigator.clipboard.write([itemInput]);
`);
});
return clipboardText;
}, "clipboard.write()");
testPasteContextMenuSuppression(async aBrowser => {
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
let div = content.document.createElement("div");
div.innerText = text;
content.document.documentElement.appendChild(div);
// select text
content
.getSelection()
.setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0);
});
// trigger keyboard shortcut to copy.
await EventUtils.synthesizeAndWaitKey(
"c",
kIsMac ? { accelKey: true } : { ctrlKey: true }
);
return clipboardText;
}, "keyboard shortcut");
testPasteContextMenuSuppression(async aBrowser => {
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
return content.eval(`
document.addEventListener("copy", function(e) {
e.preventDefault();
e.clipboardData.setData("text/plain", "${text}");
}, { once: true });
`);
});
// trigger keyboard shortcut to copy.
await EventUtils.synthesizeAndWaitKey(
"c",
kIsMac ? { accelKey: true } : { ctrlKey: true }
);
return clipboardText;
}, "keyboard shortcut with custom data");
testPasteContextMenuSuppression(async aBrowser => {
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
let div = content.document.createElement("div");
div.innerText = text;
content.document.documentElement.appendChild(div);
// select text
content
.getSelection()
.setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0);
return SpecialPowers.doCommand(content, "cmd_copy");
});
return clipboardText;
}, "copy command");
async function readTypes(aBrowser) {
return SpecialPowers.spawn(aBrowser, [], async () => {
content.document.notifyUserGestureActivation();
let items = await content.eval(`navigator.clipboard.read();`);
return items[0].types;
});
}
add_task(async function test_context_menu_suppression_image() {
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
await SpecialPowers.spawn(browser, [], async () => {
let image = content.document.createElement("img");
let copyImagePromise = new Promise(resolve => {
image.addEventListener(
"load",
e => {
let documentViewer = content.docShell.docViewer.QueryInterface(
SpecialPowers.Ci.nsIDocumentViewerEdit
);
documentViewer.setCommandNode(image);
documentViewer.copyImage(documentViewer.COPY_IMAGE_ALL);
resolve();
},
{ once: true }
);
});
image.src =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABHCAIAAADQjmMaAA" +
"AACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3goUAwAgSAORBwAAABl0RVh0Q29tbW" +
"VudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABPSURBVGje7c4BDQAACAOga//OmuMbJG" +
"AurTbq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u" +
"rq6s31B0IqAY2/tQVCAAAAAElFTkSuQmCC";
content.document.documentElement.appendChild(image);
await copyImagePromise;
});
info("Test read from cross-origin frame");
let pasteButtonIsShown = waitForPasteContextMenu();
let readTypesRequest1 = readTypes(browser.browsingContext.children[1]);
await pasteButtonIsShown;
info("Test read from same-origin frame before paste contextmenu is closed");
const clipboarCacheEnabled = SpecialPowers.getBoolPref(
"widget.clipboard.use-cached-data.enabled",
false
);
// If the cached data is used, it uses type order in cached transferable.
SimpleTest.isDeeply(
await readTypes(browser.browsingContext.children[0]),
clipboarCacheEnabled
? ["text/plain", "text/html", "image/png"]
: ["text/html", "text/plain", "image/png"],
"read from same-origin should just be resolved without showing paste contextmenu shown"
);
info("Dismiss paste button, cross-origin request should be rejected");
await promiseDismissPasteButton();
// XXX edgar: not sure why first promiseDismissPasteButton doesn't work on Windows opt build.
await promiseDismissPasteButton();
await Assert.rejects(
readTypesRequest1,
/NotAllowedError/,
"cross-origin request should be rejected"
);
});
});

Просмотреть файл

@ -0,0 +1,156 @@
/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
requestLongerTimeout(2);
const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"https://example.com"
);
const kContentFileName = "file_toplevel.html";
const kContentFileUrl = kBaseUrlForContent + kContentFileName;
const kIsMac = navigator.platform.indexOf("Mac") > -1;
async function waitForPasteContextMenu() {
await waitForPasteMenuPopupEvent("shown");
let pasteButton = document.getElementById(kPasteMenuItemId);
info("Wait for paste button enabled");
await BrowserTestUtils.waitForMutationCondition(
pasteButton,
{ attributeFilter: ["disabled"] },
() => !pasteButton.disabled,
"Wait for paste button enabled"
);
}
async function readText(aBrowser) {
return SpecialPowers.spawn(aBrowser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
}
async function testPasteContextMenu(
aBrowser,
aClipboardText,
aShouldShow = true
) {
let pasteButtonIsShown;
if (aShouldShow) {
pasteButtonIsShown = waitForPasteContextMenu();
}
let readTextRequest = readText(aBrowser);
if (aShouldShow) {
await pasteButtonIsShown;
}
info("Click paste button, request should be resolved");
if (aShouldShow) {
await promiseClickPasteButton();
}
is(await readTextRequest, aClipboardText, "Request should be resolved");
}
async function installAndStartExtension(aContentScript) {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
js: ["content_script.js"],
matches: ["https://example.com/*/file_toplevel.html"],
},
],
},
files: {
"content_script.js": aContentScript,
},
});
await extension.startup();
return extension;
}
function testExtensionContentScript(aContentScript, aMessage) {
add_task(async function test_context_menu_suppression_ext() {
info(`${aMessage}`);
const extension = await installAndStartExtension(aContentScript);
await BrowserTestUtils.withNewTab(
kContentFileUrl,
async function (browser) {
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(browser, [clipboardText], async text => {
info(`Set clipboard text to ${text}`);
let div = content.document.createElement("div");
div.id = "container";
div.innerText = text;
content.document.documentElement.appendChild(div);
});
let writePromise = extension.awaitMessage("write-data-ready");
// trigger keyboard shortcut to copy.
await EventUtils.synthesizeAndWaitKey(
"c",
kIsMac ? { accelKey: true } : { ctrlKey: true }
);
// Wait a bit for clipboard write.
await writePromise;
info("Test read from same frame");
await testPasteContextMenu(browser, clipboardText, false);
info("Test read from same-origin subframe");
await testPasteContextMenu(
browser.browsingContext.children[0],
clipboardText,
false
);
info("Test read from cross-origin subframe");
await testPasteContextMenu(
browser.browsingContext.children[1],
clipboardText,
true
);
}
);
await extension.unload();
});
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["dom.events.asyncClipboard.readText", true],
["dom.events.asyncClipboard.clipboardItem", true],
["test.events.async.enabled", true],
// Avoid paste button delay enabling making test too long.
["security.dialog_enable_delay", 0],
],
});
});
testExtensionContentScript(() => {
document.addEventListener("copy", function (e) {
e.preventDefault();
let div = document.getElementById("container");
let text = div.innerText;
e.clipboardData.setData("text/plain", text);
browser.test.sendMessage("write-data-ready");
});
}, "Write data by DataTransfer API in extension");
testExtensionContentScript(() => {
document.addEventListener("copy", async function (e) {
e.preventDefault();
let div = document.getElementById("container");
let text = div.innerText;
await navigator.clipboard.writeText(text);
browser.test.sendMessage("write-data-ready");
});
}, "Write data by Async Clipboard API in extension");

Просмотреть файл

@ -176,20 +176,14 @@ add_task(async function test_multiple_readText_from_cross_origin_frame() {
"readText() from different origin child frame again before interacting with paste button"
);
const crossOriginFrame = browser.browsingContext.children[1];
const readTextRequest2 = SpecialPowers.spawn(
crossOriginFrame,
[],
async () => {
await Assert.rejects(
SpecialPowers.spawn(crossOriginFrame, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
}
}),
/NotAllowedError/,
"Second request should be rejected"
);
// Give some time for the second request to arrive parent process.
await SpecialPowers.spawn(crossOriginFrame, [], async () => {
return new Promise(resolve => {
content.setTimeout(resolve, 0);
});
});
info("Click paste button, both request should be resolved");
await promiseClickPasteButton();
@ -198,11 +192,6 @@ add_task(async function test_multiple_readText_from_cross_origin_frame() {
clipboardText,
"First request should be resolved"
);
is(
await readTextRequest2,
clipboardText,
"Second request should be resolved"
);
});
});

Просмотреть файл

@ -100,16 +100,24 @@ function isCloselyLeftOnTopOf(aCoordsP1, aCoordsP2, aDelta = 10) {
);
}
function promiseDismissPasteButton() {
async function promiseDismissPasteButton() {
// nsXULPopupManager rollup is handled in widget code, so we have to
// synthesize native mouse events.
return EventUtils.promiseNativeMouseEvent({
await EventUtils.promiseNativeMouseEvent({
type: "click",
target: document.body,
// Relies on the assumption that the center of chrome document doesn't
// overlay with the paste button showed for clipboard readText request.
atCenter: true,
});
// Move mouse away to avoid subsequence tests showing paste button in
// thie dismissing location.
await EventUtils.promiseNativeMouseEvent({
type: "mousemove",
target: document.body,
offsetX: 100,
offsetY: 100,
});
}
// @param aBrowser browser object of the content tab.

Просмотреть файл

@ -3453,7 +3453,9 @@ mozilla::ipc::IPCResult ContentParent::RecvSetClipboard(
// aRequestingPrincipal is allowed to be nullptr here.
if (!ValidatePrincipal(aTransferable.requestingPrincipal(),
{ValidatePrincipalOptions::AllowNullPtr})) {
{ValidatePrincipalOptions::AllowNullPtr,
ValidatePrincipalOptions::AllowExpanded,
ValidatePrincipalOptions::AllowSystem})) {
LogAndAssertFailedPrincipalValidationInfo(
aTransferable.requestingPrincipal(), __func__);
}

Просмотреть файл

@ -58,7 +58,9 @@ IPCResult ClipboardWriteRequestParent::RecvSetData(
const IPCTransferable& aTransferable) {
if (!mManager->ValidatePrincipal(
aTransferable.requestingPrincipal(),
{ContentParent::ValidatePrincipalOptions::AllowNullPtr})) {
{ContentParent::ValidatePrincipalOptions::AllowNullPtr,
ContentParent::ValidatePrincipalOptions::AllowExpanded,
ContentParent::ValidatePrincipalOptions::AllowSystem})) {
ContentParent::LogAndAssertFailedPrincipalValidationInfo(
aTransferable.requestingPrincipal(), __func__);
}

Просмотреть файл

@ -52,9 +52,11 @@ class UserConfirmationRequest final
UserConfirmationRequest(int32_t aClipboardType,
Document* aRequestingChromeDocument,
nsIPrincipal* aRequestingPrincipal,
nsBaseClipboard* aClipboard)
: mClipboardType(aClipboardType),
mRequestingChromeDocument(aRequestingChromeDocument),
mRequestingPrincipal(aRequestingPrincipal),
mClipboard(aClipboard) {
MOZ_ASSERT(
mClipboard->nsIClipboard::IsClipboardTypeSupported(aClipboardType));
@ -66,10 +68,11 @@ class UserConfirmationRequest final
void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
mozilla::ErrorResult& aRv) override;
bool IsEqual(int32_t aClipboardType,
Document* aRequestingChromeDocument) const {
bool IsEqual(int32_t aClipboardType, Document* aRequestingChromeDocument,
nsIPrincipal* aRequestingPrincipal) const {
return ClipboardType() == aClipboardType &&
RequestingChromeDocument() == aRequestingChromeDocument;
RequestingChromeDocument() == aRequestingChromeDocument &&
RequestingPrincipal()->Equals(aRequestingPrincipal);
}
int32_t ClipboardType() const { return mClipboardType; }
@ -78,6 +81,8 @@ class UserConfirmationRequest final
return mRequestingChromeDocument;
}
nsIPrincipal* RequestingPrincipal() const { return mRequestingPrincipal; }
void AddClipboardGetRequest(const nsTArray<nsCString>& aFlavorList,
nsIAsyncClipboardGetCallback* aCallback) {
MOZ_ASSERT(!aFlavorList.IsEmpty());
@ -116,6 +121,7 @@ class UserConfirmationRequest final
const int32_t mClipboardType;
RefPtr<Document> mRequestingChromeDocument;
const nsCOMPtr<nsIPrincipal> mRequestingPrincipal;
const RefPtr<nsBaseClipboard> mClipboard;
// Track the pending read requests that wait for user confirmation.
nsTArray<UniquePtr<ClipboardGetRequest>> mPendingClipboardGetRequests;
@ -502,6 +508,22 @@ NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
return NS_OK;
}
// If cache data is valid, we are the last ones to put something on the native
// clipboard, then check if the data is from the same-origin page,
if (auto* clipboardCache = GetClipboardCacheIfValid(aWhichClipboard)) {
nsCOMPtr<nsITransferable> trans = clipboardCache->GetTransferable();
MOZ_ASSERT(trans);
if (nsCOMPtr<nsIPrincipal> principal = trans->GetRequestingPrincipal()) {
if (aRequestingPrincipal->Subsumes(principal)) {
MOZ_CLIPBOARD_LOG("%s: native clipboard data is from same-origin page.",
__FUNCTION__);
AsyncGetDataInternal(aFlavorList, aWhichClipboard, aCallback);
return NS_OK;
}
}
}
// TODO: enable showing the "Paste" button in this case; see bug 1773681.
if (aRequestingPrincipal->GetIsAddonOrExpandedAddonPrincipal()) {
MOZ_CLIPBOARD_LOG("%s: Addon without read permission.", __FUNCTION__);
@ -509,7 +531,8 @@ NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
}
RequestUserConfirmation(aWhichClipboard, aFlavorList,
aRequestingWindowContext, aCallback);
aRequestingWindowContext, aRequestingPrincipal,
aCallback);
return NS_OK;
}
@ -724,6 +747,7 @@ void nsBaseClipboard::ClearClipboardCache(int32_t aClipboardType) {
void nsBaseClipboard::RequestUserConfirmation(
int32_t aClipboardType, const nsTArray<nsCString>& aFlavorList,
mozilla::dom::WindowContext* aWindowContext,
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback) {
MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
MOZ_ASSERT(aCallback);
@ -760,7 +784,8 @@ void nsBaseClipboard::RequestUserConfirmation(
// If there is a pending user confirmation request, check if we could reuse
// it. If not, reject the request.
if (sUserConfirmationRequest) {
if (sUserConfirmationRequest->IsEqual(aClipboardType, chromeDoc)) {
if (sUserConfirmationRequest->IsEqual(aClipboardType, chromeDoc,
aRequestingPrincipal)) {
sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback);
return;
}
@ -784,8 +809,8 @@ void nsBaseClipboard::RequestUserConfirmation(
return;
}
sUserConfirmationRequest =
new UserConfirmationRequest(aClipboardType, chromeDoc, this);
sUserConfirmationRequest = new UserConfirmationRequest(
aClipboardType, chromeDoc, aRequestingPrincipal, this);
sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback);
promise->AppendNativeHandler(sUserConfirmationRequest);
}

Просмотреть файл

@ -197,6 +197,7 @@ class nsBaseClipboard : public nsIClipboard {
void RequestUserConfirmation(int32_t aClipboardType,
const nsTArray<nsCString>& aFlavorList,
mozilla::dom::WindowContext* aWindowContext,
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback);
// Track the pending request for each clipboard type separately. And only need

Просмотреть файл

@ -194,12 +194,17 @@ interface nsITransferable : nsISupports
[notxpcom, nostdcall] attribute boolean isPrivateData;
/**
* The principal of the source dom node this transferable was
* created from and the contentPolicyType for the transferable.
* Note, currently only used on Windows for network principal and
* contentPolicyType information in clipboard and drag operations.
* The principal associated with this transferable. This could be either the
* node principal of the source DOM node from which this transferable was
* created, or the principal of the global from which this transferable was
* created.
* XXXedgar: Rename it to something more generic, bug 1867636.
*/
[notxpcom, nostdcall] attribute nsIPrincipal requestingPrincipal;
/**
* the contentPolicyType for this transferable.
*/
[notxpcom, nostdcall] attribute nsContentPolicyType contentPolicyType;
/**