Files
sousa-gecko/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_preloading.js
T
Nika Layzell e79a04c654 Bug 2005002 - Part 3: Extension content scripts for Blob URLs no longer match with full paths, r=extension-reviewers,robwu
In part 2, the way principals for a Blob URL were inferred has been changed
such that it is no longer identical to the principal from the creator document.
This means that the exact path which was used to create the BlobURL is no
longer preserved in the BlobURL's principal.

This in turn means that some of the undocumented behaviour around blob URLs and
content script matching have changed, requiring some test changes.

Differential Revision: https://phabricator.services.mozilla.com/D276056
2026-04-24 19:42:09 +00:00

739 lines
26 KiB
JavaScript

"use strict";
/**
* Extension content scripts execution happens in two stages:
* 1. preload content script when a request for a document is observed.
* 2. actual content script execution when the document has loaded.
*
* This is generally an internal implementation detail and an optimization, but
* if it does not happen, it may lead to difficult-to-diagnose intermittent
* failures as seen at https://bugzilla.mozilla.org/show_bug.cgi?id=1583700#c12
* This test hooks the internal content script execution mechanism to confirm
* that preloading happens as expected.
*
* There are some cases where preload is triggered unexpectedly (or not),
* especially when null principals (e.g. sandboxed frames) are involved,
* or when principal-inheriting URLs (e.g. about:blank and blob:) are involved.
* This is not ideal but as long as we don't execute when we should not, we are
* good. See comment at DocInfo::PrincipalURL in WebExtensionPolicy.cpp
*/
const server = createHttpServer({ hosts: ["example.com"] });
let gRequestCount = 0;
server.registerPathHandler("/dummy", (request, response) => {
++gRequestCount;
response.setStatusLine(request.httpVersion, 200, "OK");
// The test will add iframes.
response.write("(dummy, no iframes from server)");
});
server.registerPathHandler("/sandboxed", (request, response) => {
++gRequestCount;
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Security-Policy", "sandbox allow-scripts;");
response.write("This page has an opaque origin.");
});
// Ensure that we can detect content script injections for the given extension
// in the process that is currently loaded in |contentPage|.
async function ensureContentScriptDetector(extension, contentPage) {
await contentPage.spawn([extension.id], extensionId => {
const { ExtensionProcessScript } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionProcessScript.sys.mjs"
);
function log(contentScript, isPreload) {
const policy = contentScript.extension;
if (policy.id === extensionId) {
policy._testOnlySeenContentScriptInjections ??= [];
policy._testOnlySeenContentScriptInjections.push({
matches: contentScript.matches.patterns.map(p => p.pattern),
isPreload,
});
}
}
// Note: we overwrite the ExtensionProcessScript methods without a way to
// undo that in this test. The helper is carefully designed to avoid any
// side effects other than on the specified extension.
const { preloadContentScript, loadContentScript } = ExtensionProcessScript;
ExtensionProcessScript.preloadContentScript = contentScript => {
log(contentScript, true);
return preloadContentScript(contentScript);
};
ExtensionProcessScript.loadContentScript = (contentScript, window) => {
log(contentScript, false);
return loadContentScript(contentScript, window);
};
});
}
async function getSeenContentScriptInjections(extension, contentPage) {
return contentPage.spawn([extension.id], extensionId => {
let policy = this.content.WebExtensionPolicy.getByID(extensionId);
// Clear the logs and return what we had.
return policy._testOnlySeenContentScriptInjections?.splice(0);
});
}
add_task(async function test_preload_at_http_toplevel() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/dummy?toplevelonly"],
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/dummy?toplevelonly");
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?toplevelonly"], isPreload: true },
{ matches: ["*://example.com/dummy?toplevelonly"], isPreload: false },
],
"Should have observed preload for http toplevel navigation"
);
await contentPage.close();
await extension.unload();
});
add_task(async function test_preload_at_http_iframe() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/dummy?in_iframe"],
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?toplevel-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.src = "http://example.com/dummy?in_iframe";
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?in_iframe"], isPreload: true },
{ matches: ["*://example.com/dummy?in_iframe"], isPreload: false },
],
"Should have observed preload for http frame"
);
await contentPage.close();
await extension.unload();
});
// Verify that content script preloading and loading works in http:-frame
// inserted by a content script. This is special because a content script has
// an expanded principal.
add_task(async function test_preload_at_http_iframe_from_content_script() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/dummy?do_load_frame"],
js: ["do_load_frame.js"],
run_at: "document_end",
},
{
matches: ["*://example.com/dummy?in_frame_from_cs"],
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"do_load_frame.js": () => {
let f = document.createElement("iframe");
f.src = "http://example.com/dummy?in_frame_from_cs";
document.body.append(f);
},
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/dummy?do_load_frame");
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?do_load_frame"], isPreload: true },
{ matches: ["*://example.com/dummy?do_load_frame"], isPreload: false },
{ matches: ["*://example.com/dummy?in_frame_from_cs"], isPreload: true },
{ matches: ["*://example.com/dummy?in_frame_from_cs"], isPreload: false },
],
"Should have observed preload for http frame injected by content script"
);
await contentPage.close();
await extension.unload();
});
// Start extension whose content script executes in a page that existed prior
// to extension startup. Should execute immediately without preloading.
add_task(async function test_no_preload_in_existing_documents() {
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?existing_doc"
);
const extensionId = "@extension-with-content-script-in-existing-doc";
await ensureContentScriptDetector({ id: extensionId }, contentPage);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id: extensionId } },
content_scripts: [
{
matches: ["*://example.com/dummy?existing_doc"],
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[{ matches: ["*://example.com/dummy?existing_doc"], isPreload: false }],
"Should not preload when scripts are executed as part of extension startup"
);
await contentPage.close();
await extension.unload();
});
// No preload for content scripts at about:blank.
// But preload at about:srcdoc is supported (due to principal inheritance).
add_task(async function test_preload_at_about_blank_iframe() {
let extension = ExtensionTestUtils.loadExtension({
isPrivileged: true, // For "about:*" match pattern sanity check below.
manifest: {
content_scripts: [
{
matches: ["*://example.com/dummy?with_blank"],
match_about_blank: true,
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
matches: ["*://example.com/*"],
exclude_matches: ["*://example.com/dummy?initial-without_cs"],
match_origin_as_fallback: true,
all_frames: true,
js: ["done2.js"],
run_at: "document_end",
},
{
// about:blank / about:srcdoc matching is based on the principal URL.
// To verify that the principal URL is used instead of about:blank at
// preloading, add a script that should not match.
// If preloading were to use the incorrect URL, then this script
// would unexpectedly execute.
// note: "about:*" match pattern requires isPrivileged:true.
matches: ["about:*"],
match_about_blank: true,
match_origin_as_fallback: true,
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
// The match_origin_as_fallback option should supersede
// match_about_blank. Despite match_about_blank being set to true,
// it should be interpreted as false because match_origin_as_fallback
// was explicitly set to false.
matches: ["*://example.com/*", "*://3/"], // 3 to distinguish from ^.
exclude_matches: ["*://example.com/dummy?initial-without_cs"],
match_about_blank: true,
match_origin_as_fallback: false,
all_frames: true,
js: ["done3.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
"done2.js": `browser.test.sendMessage("script_run2");`,
"done3.js": `browser.test.sendMessage("script_run3");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/dummy?with_blank");
await extension.awaitMessage("script_run");
await extension.awaitMessage("script_run2");
await extension.awaitMessage("script_run3");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?with_blank"], isPreload: true },
{ matches: ["*://example.com/*"], isPreload: true },
{ matches: ["*://example.com/*", "*://3/"], isPreload: true },
{ matches: ["*://example.com/dummy?with_blank"], isPreload: false },
{ matches: ["*://example.com/*"], isPreload: false },
{ matches: ["*://example.com/*", "*://3/"], isPreload: false },
],
"Should have observed preload in initial http document"
);
// Setup done, now let's run the actual test!
info("Testing about:blank frame with match_about_blank");
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
await extension.awaitMessage("script_run2");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
// We don't preload even though we could. See comment at top of file.
{ matches: ["*://example.com/dummy?with_blank"], isPreload: false },
{ matches: ["*://example.com/*"], isPreload: false },
],
"Preloading is NOT supported in about:blank"
);
info("Testing javascript: frame with match_about_blank");
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.src = "javascript:'javascript:-URL loads in about:blank doc'";
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
await extension.awaitMessage("script_run2");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
// We don't preload even though we could. See comment at top of file.
{ matches: ["*://example.com/dummy?with_blank"], isPreload: false },
{ matches: ["*://example.com/*"], isPreload: false },
],
"Preloading is NOT supported in about:blank (javascript:-URL)"
);
info("Testing about:srcdoc frame with match_about_blank");
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.srcdoc = "This is about:srcdoc";
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
await extension.awaitMessage("script_run2");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?with_blank"], isPreload: true },
{ matches: ["*://example.com/*"], isPreload: true },
{ matches: ["*://example.com/dummy?with_blank"], isPreload: false },
{ matches: ["*://example.com/*"], isPreload: false },
],
"Preloading is supported in about:srcdoc"
);
info("Testing sandboxed about:srcdoc frame with match_about_blank");
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.srcdoc = "This is about:srcdoc in a sandbox";
f.sandbox = "allow-scripts"; // no allow-same-origin.
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run2");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
// We preload when we should not. See comment at top of file.
{ matches: ["*://example.com/dummy?with_blank"], isPreload: true },
// With match_origin_as_fallback, we should inject despite the opaque
// origin of the sandbox:.
{ matches: ["*://example.com/*"], isPreload: true },
{ matches: ["*://example.com/*"], isPreload: false },
],
"sandboxed about:srcdoc requires match_origin_as_fallback:true"
);
await contentPage.close();
await extension.unload();
});
// No preload for content scripts at blob:-URLs.
add_task(async function test_no_preload_at_blob_url_iframe() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/*"],
exclude_matches: ["*://example.com/dummy?initial-without_cs"],
match_origin_as_fallback: true,
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/dummy?with_blob_url");
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/*"], isPreload: true },
{ matches: ["*://example.com/*"], isPreload: false },
],
"Should have observed preload in initial http document"
);
// Setup done, now let's run the actual test!
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.src = this.content.eval(`URL.createObjectURL(new Blob(["blob:-doc"]))`);
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
// We don't preload even though we could. See comment at top of file.
{ matches: ["*://example.com/*"], isPreload: false },
],
"Preloading is NOT supported in blob:-URLs"
);
await contentPage.close();
await extension.unload();
});
// A sandboxed page has an opaque origin. A http page can be sandboxed by
// the "sandbox" attribute on an iframe.
add_task(async function test_preload_at_http_iframe_with_sandbox_attr() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/dummy?sandbox_parent"],
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
matches: ["*://example.com/dummy?sandbox_iframe"],
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
// Sanity check: should not execute.
matches: ["<all_urls>"],
exclude_matches: [
"*://example.com/dummy?initial-without_cs",
"*://example.com/dummy?sandbox_parent",
"*://example.com/dummy?sandbox_iframe",
],
match_about_blank: true,
match_origin_as_fallback: true,
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/dummy?sandbox_parent");
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?sandbox_parent"], isPreload: true },
{ matches: ["*://example.com/dummy?sandbox_parent"], isPreload: false },
],
"Should have observed preload in initial http document"
);
// Setup done, now let's run the actual test!
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.src = "http://example.com/dummy?sandbox_iframe";
f.sandbox = "allow-scripts"; // no allow-same-origin.
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?sandbox_iframe"], isPreload: true },
{ matches: ["*://example.com/dummy?sandbox_iframe"], isPreload: false },
],
"Should not observe any loads of non-matching sandboxed http document"
);
await contentPage.close();
await extension.unload();
});
// A sandboxed page has an opaque origin. A http page can be sandboxed by
// the "sandbox" directive in the response header.
add_task(async function test_preload_at_http_csp_sandbox() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/sandboxed"],
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
// Sanity check: should not execute.
matches: ["<all_urls>"],
exclude_matches: [
"*://example.com/dummy?initial-without_cs",
"*://example.com/sandboxed",
],
match_about_blank: true,
match_origin_as_fallback: true,
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/sandboxed");
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/sandboxed"], isPreload: true },
{ matches: ["*://example.com/sandboxed"], isPreload: false },
],
"Should not observe any loads of non-matching sandboxed http document"
);
await contentPage.close();
await extension.unload();
});
add_task(async function test_preload_at_data_url() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/dummy?data_parent"],
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
// Sanity check: should not execute.
matches: ["<all_urls>", "*://1/"], // 1 to distinguish from below.
exclude_matches: [
"*://example.com/dummy?initial-without_cs",
"*://example.com/dummy?data_parent",
],
match_about_blank: true, // instead of match_origin_as_fallback:true.
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
{
matches: ["<all_urls>", "*://2/"], // 2 to distinguish from above.
exclude_matches: [
"*://example.com/dummy?initial-without_cs",
"*://example.com/dummy?data_parent",
],
match_origin_as_fallback: true, // instead of match_about_blank:true.
all_frames: true,
js: ["done.js"],
run_at: "document_end",
},
],
},
files: {
"done.js": `browser.test.sendMessage("script_run");`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
"http://example.com/dummy?initial-without_cs"
);
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("http://example.com/dummy?data_parent");
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
{ matches: ["*://example.com/dummy?data_parent"], isPreload: true },
{ matches: ["*://example.com/dummy?data_parent"], isPreload: false },
],
"Should have observed preload in initial http document"
);
// Setup done, now let's run the actual test!
info("Testing plain data:-URL in iframe");
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.src = "data:,data_url_in_iframe";
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
// We preload when we should not. See comment at top of file.
{ matches: ["<all_urls>", "*://1/"], isPreload: true },
// With match_origin_as_fallback:true, this is expected:
{ matches: ["<all_urls>", "*://2/"], isPreload: true },
{ matches: ["<all_urls>", "*://2/"], isPreload: false },
],
"Should match data:-URI when match_origin_as_fallback is true"
);
info("Testing sandboxed data:-URL in iframe");
await contentPage.spawn([], () => {
let f = this.content.wrappedJSObject.document.createElement("iframe");
f.src = "data:,data_url_in_sandboxed_iframe";
f.sandbox = "allow-scripts"; // no allow-same-origin.
this.content.wrappedJSObject.document.body.append(f);
});
await extension.awaitMessage("script_run");
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
[
// We preload when we should not. See comment at top of file.
{ matches: ["<all_urls>", "*://1/"], isPreload: true },
// With match_origin_as_fallback:true, this is expected:
{ matches: ["<all_urls>", "*://2/"], isPreload: true },
{ matches: ["<all_urls>", "*://2/"], isPreload: false },
],
"Should match sandboxed data:-URI when match_origin_as_fallback is true"
);
await contentPage.close();
await extension.unload();
});
// No execution nor preload for content scripts at view-source:-URLs.
add_task(async function test_no_preload_nor_execution_at_view_source() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
matches: ["*://example.com/*"],
match_origin_as_fallback: true,
js: ["done.js"],
run_at: "document_start",
},
],
},
files: {
"done.js": `browser.test.fail("Unexpected:: " + origin + document.URL);`,
},
});
await extension.startup();
gRequestCount = 0;
// Load initial view-source to get the right process.
let contentPage = await ExtensionTestUtils.loadContentPage(
"view-source:http://example.com/dummy?initial-viewSource"
);
await ensureContentScriptDetector(extension, contentPage);
// Since we are not expecting content script execution, there is no event to
// wait for, so we just load another page to maximize the chance of catching
// an unexpected content script, if any.
await contentPage.loadURL("view-source:http://example.com/dummy?viewSource");
// Also test sandboxed origins (regression test for bug 1897759).
await contentPage.loadURL("view-source:http://example.com/sandboxed?viewS1");
// Call ensureContentScriptDetector() again to make sure that we can detect
// content scripts even if sandboxed origins somehow get their own process.
await ensureContentScriptDetector(extension, contentPage);
await contentPage.loadURL("view-source:http://example.com/sandboxed?viewS2");
Assert.equal(gRequestCount, 4, "Got two view-source requests.");
gRequestCount = 0;
Assert.deepEqual(
await getSeenContentScriptInjections(extension, contentPage),
undefined,
"Should not have observed any content scripts at view-source:-URLs"
);
await contentPage.close();
await extension.unload();
});