Bug 1914068 - pass correct URL to Content Analysis for same-origin iframes r=dlp-reviewers,win-reviewers,handyman

Differential Revision: https://phabricator.services.mozilla.com/D220168
This commit is contained in:
Greg Stoll 2024-09-04 19:55:29 +00:00
Родитель 497ad2d813
Коммит b8436fd050
13 изменённых файлов: 445 добавлений и 10 удалений

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

@ -1778,7 +1778,15 @@ ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed(
__func__);
return;
}
nsCOMPtr<nsIURI> uri = windowParent->GetDocumentURI();
nsCOMPtr<nsIURI> uri = GetURIForBrowsingContext(
windowParent->Canonical()->GetBrowsingContext());
if (!uri) {
promise->Reject(
PrintAllowedError(NS_ERROR_FAILURE,
cachedStaticBrowsingContext),
__func__);
return;
}
nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
new contentanalysis::ContentAnalysisRequest(
std::move(printData), std::move(uri),
@ -2045,7 +2053,13 @@ void ContentAnalysis::CheckClipboardContentAnalysis(
}
}
nsCOMPtr<nsIURI> currentURI = aWindow->Canonical()->GetDocumentURI();
nsCOMPtr<nsIURI> currentURI =
GetURIForBrowsingContext(aWindow->Canonical()->GetBrowsingContext());
if (!currentURI) {
aResolver->Callback(ContentAnalysisResult::FromNoResult(
NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR));
return;
}
nsTArray<nsCString> flavors;
rv = aTransferable->FlavorsTransferableCanExport(flavors);
if (NS_WARN_IF(NS_FAILED(rv))) {
@ -2233,6 +2247,46 @@ ContentAnalysis::GetDiagnosticInfo(JSContext* aCx,
return NS_OK;
}
/* static */ nsCOMPtr<nsIURI> ContentAnalysis::GetURIForBrowsingContext(
dom::CanonicalBrowsingContext* aBrowsingContext) {
dom::WindowGlobalParent* windowGlobal =
aBrowsingContext->GetCurrentWindowGlobal();
if (!windowGlobal) {
return nullptr;
}
nsIPrincipal* principal = windowGlobal->DocumentPrincipal();
dom::CanonicalBrowsingContext* curBrowsingContext =
aBrowsingContext->GetParent();
while (curBrowsingContext) {
dom::WindowGlobalParent* newWindowGlobal =
curBrowsingContext->GetCurrentWindowGlobal();
if (!newWindowGlobal) {
break;
}
nsIPrincipal* newPrincipal = newWindowGlobal->DocumentPrincipal();
if (!(newPrincipal->Subsumes(principal))) {
break;
}
principal = newPrincipal;
curBrowsingContext = curBrowsingContext->GetParent();
}
return principal->GetURI();
}
// IDL implementation
NS_IMETHODIMP ContentAnalysis::GetURIForBrowsingContext(
dom::BrowsingContext* aBrowsingContext, nsIURI** aURI) {
NS_ENSURE_ARG_POINTER(aBrowsingContext);
NS_ENSURE_ARG_POINTER(aURI);
nsCOMPtr<nsIURI> uri =
GetURIForBrowsingContext(aBrowsingContext->Canonical());
if (!uri) {
return NS_ERROR_FAILURE;
}
uri.forget(aURI);
return NS_OK;
}
NS_IMETHODIMP ContentAnalysisCallback::ContentResult(
nsIContentAnalysisResponse* aResponse) {
if (mPromise.isSome()) {

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

@ -217,6 +217,11 @@ class ContentAnalysis final : public nsIContentAnalysis {
mResolver;
};
static bool MightBeActive();
// Find the outermost browsing context that has same-origin access to
// aBrowsingContext, and this is the URL we will pass to the Content Analysis
// agent.
static nsCOMPtr<nsIURI> GetURIForBrowsingContext(
dom::CanonicalBrowsingContext* aBrowsingContext);
static bool CheckClipboardContentAnalysisSync(
nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow,
const nsCOMPtr<nsITransferable>& trans,

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

@ -6,6 +6,7 @@
#include "nsISupports.idl"
interface nsIURI;
webidl BrowsingContext;
webidl WindowGlobalParent;
[scriptable, uuid(06e6a60f-3a2b-41fa-a63b-fea7a7f71649)]
@ -295,4 +296,10 @@ interface nsIContentAnalysis : nsISupports
*/
[implicit_jscontext]
Promise getDiagnosticInfo();
/**
* Gets the URI to use for the passed-in browsing context. This correctly
* handles iframes.
*/
nsIURI getURIForBrowsingContext(in BrowsingContext aBrowsingContext);
};

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

@ -16,6 +16,12 @@ support-files = [
"clipboard_paste_inputandtextarea.html",
]
["browser_clipboard_paste_inputandtextarea_iframe_content_analysis.js"]
support-files = [
"clipboard_paste_inputandtextarea.html",
"clipboard_paste_inputandtextarea_containing_frame.html",
]
["browser_clipboard_paste_noformatting_content_analysis.js"]
support-files = [
"clipboard_paste_noformatting.html",
@ -45,3 +51,10 @@ support-files = [
"!/toolkit/components/printing/tests/longerArticle.html",
"!/toolkit/components/printing/tests/simplifyArticleSample.html",
]
["browser_print_iframe_content_analysis.js"]
support-files = [
"!/toolkit/components/printing/tests/head.js",
"!/toolkit/components/printing/tests/simplifyArticleSample.html",
"clipboard_print_iframe.html",
]

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

@ -360,10 +360,11 @@ async function testClipboardWithContentAnalysis(allowPaste) {
}
function assertContentAnalysisRequest(request, expectedText) {
is(
request.url.spec,
"data:text/html," + escape(testPage),
"request has correct URL"
// This page is loaded via a data: URL which has a null principal,
// so the URL will reflect this.
ok(
request.url.spec.startsWith("moz-nullprincipal:"),
"request has correct moz-nullprincipal URL, got " + request.url.spec
);
is(
request.analysisType,

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

@ -0,0 +1,172 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let mockCA = makeMockContentAnalysis();
add_setup(async function test_setup() {
mockCA = mockContentAnalysisService(mockCA);
});
const PAGE_URL_OUTER_SAME_ORIGIN =
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea_containing_frame.html";
const PAGE_URL_OUTER_DIFFERENT_ORIGIN =
"https://example.org/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea_containing_frame.html";
const PAGE_URL_INNER =
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea.html";
const CLIPBOARD_TEXT_STRING = "Just some text";
async function testClipboardPaste(allowPaste, sameOrigin) {
mockCA.setupForTest(allowPaste);
setClipboardData(CLIPBOARD_TEXT_STRING);
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
sameOrigin ? PAGE_URL_OUTER_SAME_ORIGIN : PAGE_URL_OUTER_DIFFERENT_ORIGIN
);
let browser = tab.linkedBrowser;
await SpecialPowers.spawn(browser, [allowPaste], async allowPaste => {
let frame = content.document.querySelector("iframe");
await SpecialPowers.spawn(frame, [allowPaste], async allowPaste => {
let doc = content.document;
if (doc.readyState !== "complete") {
await new Promise(r => {
doc.addEventListener("DOMContentLoaded", () => {
r();
});
});
}
let elem = doc.getElementById("pasteAllowed");
elem.checked = allowPaste;
});
});
await testPasteWithElementId("testInput", browser, allowPaste, sameOrigin);
await testPasteWithElementId("testTextArea", browser, allowPaste, sameOrigin);
BrowserTestUtils.removeTab(tab);
}
function setClipboardData(clipboardString) {
const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
Ci.nsITransferable
);
trans.init(null);
trans.addDataFlavor("text/plain");
const str = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
str.data = clipboardString;
trans.setTransferData("text/plain", str);
// Write to clipboard.
Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
}
async function testPasteWithElementId(
elementId,
browser,
allowPaste,
sameOrigin
) {
let resultPromise = SpecialPowers.spawn(browser, [], () => {
let frame = content.document.querySelector("iframe");
return SpecialPowers.spawn(frame, [], () => {
return new Promise(resolve => {
content.document.addEventListener(
"testresult",
event => {
resolve(event.detail.result);
},
{ once: true }
);
});
});
});
// Paste into content
await SpecialPowers.spawn(browser, [elementId], async elementId => {
let frame = content.document.querySelector("iframe");
await SpecialPowers.spawn(frame, [elementId], elementId => {
content.document.getElementById(elementId).focus();
});
});
const iframeBC = browser.browsingContext.children[0];
await BrowserTestUtils.synthesizeKey("v", { accelKey: true }, iframeBC);
let result = await resultPromise;
is(result, undefined, "Got unexpected result from page");
// Because we call event.clipboardData.getData in the test, this causes another call to
// content analysis.
is(mockCA.calls.length, 2, "Correct number of calls to Content Analysis");
assertContentAnalysisRequest(
mockCA.calls[0],
CLIPBOARD_TEXT_STRING,
sameOrigin
);
assertContentAnalysisRequest(
mockCA.calls[1],
CLIPBOARD_TEXT_STRING,
sameOrigin
);
mockCA.clearCalls();
let value = await getElementValue(browser, elementId);
is(
value,
allowPaste ? CLIPBOARD_TEXT_STRING : "",
"element has correct value"
);
}
function assertContentAnalysisRequest(request, expectedText, sameOrigin) {
// If the outer page is same-origin to the iframe, the outer page URL should be passed to Content Analysis.
// Otherwise the inner page URL should be passed.
is(
request.url.spec,
sameOrigin ? PAGE_URL_OUTER_SAME_ORIGIN : PAGE_URL_INNER,
"request has correct URL"
);
is(
request.analysisType,
Ci.nsIContentAnalysisRequest.eBulkDataEntry,
"request has correct analysisType"
);
is(
request.operationTypeForDisplay,
Ci.nsIContentAnalysisRequest.eClipboard,
"request has correct operationTypeForDisplay"
);
is(request.filePath, "", "request filePath should match");
is(request.textContent, expectedText, "request textContent should match");
is(request.printDataHandle, 0, "request printDataHandle should not be 0");
is(request.printDataSize, 0, "request printDataSize should not be 0");
ok(!!request.requestToken.length, "request requestToken should not be empty");
}
async function getElementValue(browser, elementId) {
return await SpecialPowers.spawn(browser, [elementId], async elementId => {
let frame = content.document.querySelector("iframe");
return await SpecialPowers.spawn(frame, [elementId], elementId => {
return content.document.getElementById(elementId).value;
});
});
}
add_task(async function testClipboardPasteSameOriginWithContentAnalysisAllow() {
await testClipboardPaste(true, true);
});
add_task(async function testClipboardPasteSameOriginWithContentAnalysisBlock() {
await testClipboardPaste(false, true);
});
add_task(
async function testClipboardPasteDifferentOriginWithContentAnalysisAllow() {
await testClipboardPaste(true, false);
}
);
add_task(
async function testClipboardPasteDifferentOriginWithContentAnalysisBlock() {
await testClipboardPaste(false, false);
}
);

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

@ -60,6 +60,11 @@ async function testClipboardPaste(allowPaste) {
);
is(mockCA.calls.length, 1, "Correct number of calls to Content Analysis");
assertContentAnalysisRequest(mockCA.calls[0], CLIPBOARD_TEXT_STRING);
is(
mockCA.browsingContextsForURIs.length,
1,
"Correct number of calls to getURIForBrowsingContext()"
);
BrowserTestUtils.removeTab(tab);
}

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

@ -0,0 +1,142 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/toolkit/components/printing/tests/head.js",
this
);
const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
Ci.nsIPrintSettingsService
);
let mockCA = makeMockContentAnalysis();
add_setup(async function test_setup() {
mockCA = mockContentAnalysisService(mockCA);
});
const TEST_PAGE_URL =
"https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_print_iframe.html";
const TEST_PAGE_URL_INNER =
"https://example.com/browser/toolkit/components/printing/tests/simplifyArticleSample.html";
function addUniqueSuffix(prefix) {
return `${prefix}-${Services.uuid
.generateUUID()
.toString()
.slice(1, -1)}.pdf`;
}
async function printToDestination(aBrowsingContext, aDestination) {
let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
let fileName = addUniqueSuffix(`printDestinationTest-${aDestination}`);
let filePath = PathUtils.join(tmpDir.path, fileName);
info(`Printing to ${filePath}`);
let settings = PSSVC.createNewPrintSettings();
settings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
settings.outputDestination = aDestination;
settings.headerStrCenter = "";
settings.headerStrLeft = "";
settings.headerStrRight = "";
settings.footerStrCenter = "";
settings.footerStrLeft = "";
settings.footerStrRight = "";
settings.unwriteableMarginTop = 1; /* Just to ensure settings are respected on both */
let outStream = null;
if (aDestination == Ci.nsIPrintSettings.kOutputDestinationFile) {
settings.toFileName = PathUtils.join(tmpDir.path, fileName);
} else {
is(aDestination, Ci.nsIPrintSettings.kOutputDestinationStream);
outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
Ci.nsIFileOutputStream
);
let tmpFile = tmpDir.clone();
tmpFile.append(fileName);
outStream.init(tmpFile, -1, 0o666, 0);
settings.outputStream = outStream;
}
await aBrowsingContext.print(settings);
return filePath;
}
function assertContentAnalysisRequest(request, expectedUrl) {
is(
request.url.spec,
expectedUrl ?? TEST_PAGE_URL,
"request has correct outer URL"
);
is(
request.analysisType,
Ci.nsIContentAnalysisRequest.ePrint,
"request has print analysisType"
);
is(
request.operationTypeForDisplay,
Ci.nsIContentAnalysisRequest.eOperationPrint,
"request has print operationTypeForDisplay"
);
is(request.textContent, "", "request textContent should be empty");
is(request.filePath, "", "request filePath should be empty");
isnot(request.printDataHandle, 0, "request printDataHandle should not be 0");
isnot(request.printDataSize, 0, "request printDataSize should not be 0");
ok(!!request.requestToken.length, "request requestToken should not be empty");
}
// Printing to a stream is different than going through the print preview dialog because it
// doesn't make a static clone of the document before the print, which causes the
// Content Analysis code to go through a different code path. This is similar to what
// happens when various preferences are set to skip the print preview dialog, for example
// print.prefer_system_dialog.
add_task(
async function testPrintIframeToStreamWithContentAnalysisActiveAndAllowing() {
await PrintHelper.withTestPage(
async helper => {
mockCA.setupForTest(true);
await SpecialPowers.spawn(helper.sourceBrowser, [], async () => {
let innerDoc =
content.document.querySelector("iframe").contentDocument;
if (innerDoc.readyState !== "complete") {
await new Promise(r => {
innerDoc.addEventListener("DOMContentLoaded", () => {
r();
});
});
}
});
let frameBrowsingContext =
helper.sourceBrowser.browsingContext.children[0];
let filePath = await printToDestination(
frameBrowsingContext,
Ci.nsIPrintSettings.kOutputDestinationFile
);
is(
mockCA.calls.length,
1,
"Correct number of calls to Content Analysis"
);
assertContentAnalysisRequest(mockCA.calls[0]);
await waitForFileToAlmostMatchSize(
filePath,
mockCA.calls[0].printDataSize
);
await IOUtils.remove(filePath);
},
TEST_PAGE_URL,
true
);
}
);

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

@ -0,0 +1,9 @@
<html>
<body>
<iframe id="iframe" src="https://example.com/browser/toolkit/components/contentanalysis/tests/browser/clipboard_paste_inputandtextarea.html"></iframe>
</script>
</body>
</html>

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

@ -0,0 +1,9 @@
<html>
<body>
<iframe id="iframe" src="https://example.com/browser/toolkit/components/printing/tests/simplifyArticleSample.html"></iframe>
</script>
</body>
</html>

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

@ -122,16 +122,17 @@ function makeMockContentAnalysis() {
setupForTest(shouldAllowRequest) {
this.shouldAllowRequest = shouldAllowRequest;
this.errorValue = undefined;
this.calls = [];
this.clearCalls();
},
setupForTestWithError(errorValue) {
this.errorValue = errorValue;
this.calls = [];
this.clearCalls();
},
clearCalls() {
this.calls = [];
this.browsingContextsForURIs = [];
},
getAction() {
@ -187,6 +188,14 @@ function makeMockContentAnalysis() {
cancelAllRequests() {
// This is called on exit, no need to do anything
},
getURIForBrowsingContext(aBrowsingContext) {
// The real implementation walks up the parent chain as long
// as the parent principal subsumes the child one. For testing
// purposes, just return the browsing context's URI.
this.browsingContextsForURIs.push(aBrowsingContext);
return aBrowsingContext.currentURI;
},
};
}

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

@ -158,7 +158,9 @@ function commonDialogOnLoad() {
resources: [],
analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard,
url: args.owningBrowsingContext.currentURI,
url: lazy.gContentAnalysis.getURIForBrowsingContext(
args.owningBrowsingContext
),
textContent: data,
windowGlobalParent:
args.owningBrowsingContext.currentWindowContext,

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

@ -21,6 +21,7 @@
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/Logging.h"
#include "mozilla/ipc/UtilityProcessManager.h"
#include "mozilla/ProfilerLabels.h"
@ -813,7 +814,13 @@ nsFilePicker::CheckContentAnalysisService() {
__func__);
}
nsCOMPtr<nsIURI> uri = mBrowsingContext->Canonical()->GetCurrentURI();
nsCOMPtr<nsIURI> uri =
mozilla::contentanalysis::ContentAnalysis::GetURIForBrowsingContext(
mBrowsingContext->Canonical());
if (!uri) {
return nsFilePicker::ContentAnalysisResponse::CreateAndReject(
NS_ERROR_FAILURE, __func__);
}
auto processOneItem = [self = RefPtr{this},
contentAnalysis = std::move(contentAnalysis),