Bug 1897113 - Require match_origin_as_fallback for blob:-URLs r=rpl

Differential Revision: https://phabricator.services.mozilla.com/D210638
This commit is contained in:
Rob Wu 2024-05-27 11:49:15 +00:00
parent 634f9902ab
commit cded1a802f
8 changed files with 200 additions and 12 deletions

View file

@ -5080,6 +5080,14 @@
value: false
mirror: always
# When true, content scripts of MV2 extensions can run in blob:-documents without
# requiring match_origin_as_fallback to be set, to revert bug 1897113.
# TODO bug 1899134: Remove this pref.
- name: extensions.script_blob_without_match_origin_as_fallback
type: bool
value: false
mirror: always
# Legacy behavior on filterResponse calls on intercepted sw script requests.
- name: extensions.filterResponseServiceWorkerScript.disabled
type: bool

View file

@ -57,6 +57,10 @@ class MOZ_STACK_CLASS DocInfo final {
// In all other cases, URL() is returned.
const URLInfo& PrincipalURL() const;
// Whether match_origin_as_fallback must be set in order for PrincipalURL()
// to be eligible for matching the document.
bool RequiresMatchOriginAsFallback() const;
bool IsTopLevel() const;
bool IsSameOriginWithTop() const;
bool ShouldMatchActiveTabPermission() const;
@ -94,6 +98,7 @@ class MOZ_STACK_CLASS DocInfo final {
const URLInfo mURL;
mutable Maybe<const URLInfo> mPrincipalURL;
mutable Maybe<bool> mRequiresMatchOriginAsFallback;
mutable Maybe<bool> mIsTopLevel;

View file

@ -836,9 +836,19 @@ bool MozDocumentMatcher::Matches(const DocInfo& aDoc,
// with a precursor may result in a match with the specific pattern.
}
if (!mMatchOriginAsFallback && aDoc.Principal() &&
aDoc.Principal()->GetIsNullPrincipal() && !aDoc.URL().IsNonOpaqueURL()) {
return false;
if (!mMatchOriginAsFallback && aDoc.RequiresMatchOriginAsFallback()) {
// TODO bug 1899134: We should unconditionally return false here. But we
// had accidental support for matching blob:-URLs (by the content
// principal's URL) for a long time, so we have a temporary pref to fall
// back to the original behavior if needed.
if (aDoc.URL().Scheme() != nsGkAtoms::blob || !mExtension ||
mExtension->ManifestVersion() != 2 ||
!StaticPrefs::
extensions_script_blob_without_match_origin_as_fallback()) {
return false;
}
// Fall-through implies that we have a MV2 extension and a blob:-URL, with
// extensions.script_blob_without_match_origin_as_fallback set to true.
}
if (mRestricted && WebExtensionPolicy::IsRestrictedDoc(aDoc)) {
@ -1169,5 +1179,17 @@ const URLInfo& DocInfo::PrincipalURL() const {
return mPrincipalURL.ref();
}
bool DocInfo::RequiresMatchOriginAsFallback() const {
if (mRequiresMatchOriginAsFallback.isNothing()) {
mRequiresMatchOriginAsFallback.emplace(
// Special-case blob:-URLs because their principal is indistinguishable
// from the principals that created them.
URL().Scheme() == nsGkAtoms::blob ||
(Principal() && Principal()->GetIsNullPrincipal() &&
!URL().IsNonOpaqueURL()));
}
return mRequiresMatchOriginAsFallback.ref();
}
} // namespace extensions
} // namespace mozilla

View file

@ -180,6 +180,8 @@ skip-if = [
"http2",
]
["test_ext_contentscript_blob.html"]
["test_ext_contentscript_cache.html"]
skip-if = [
"os == 'linux' && debug",

View file

@ -15,7 +15,7 @@
// Tests that match_about_blank matches at the expected URLs:
// - about:blank and about:srcdoc (as documented)
// - javascript:-URL (loaded in about:blank document)
// - blob: (not documented, not supported by Chrome, historical behavior)
// - blob: (not documented, not supported by Chrome, legacy behavior)
add_task(async function test_contentscript_about_blank() {
const manifest = {
content_scripts: [
@ -154,19 +154,21 @@ add_task(async function test_contentscript_about_blank() {
info(`Opening blob:-URL: ${blobUrl} from ${win.document.URL}`);
let blobWin = window.open(blobUrl);
// blob:-URLs should not have content scripts because
// match_origin_as_fallback is not set.
await Promise.all([
extension.awaitMessage("all:" + blobUrl),
extension.awaitMessage("all:about:blank"),
extension.awaitMessage("all:about:srcdoc"),
extension.awaitMessage("mochi_without:" + blobUrl),
extension.awaitMessage("mochi_with:" + blobUrl),
extension.awaitMessage("mochi_with:about:blank"),
extension.awaitMessage("mochi_with:about:srcdoc"),
]);
is(count, 7, "exactly 7 more scripts ran for blob:-URL");
is(count, 4, "exactly 4 more scripts ran for blob:-URL");
count = 0;
// Test coverage for execution on blob:-URLs is at
// toolkit/components/extensions/test/mochitest/test_ext_contentscript_blob.html
blobWin.close();
win.close();
await extension.unload();

View file

@ -0,0 +1,144 @@
<!doctype html>
<html>
<head>
<title>Test content scripts at blob:-URLs</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
function loadTestExtension({ manifest_version }) {
return ExtensionTestUtils.loadExtension({
manifest: {
name: "MV" + manifest_version,
manifest_version,
content_scripts: [
{
matches: ["*://mochi.test/*"],
// match_origin_as_fallback: false, // false by default
js: ["moaf_false.js"],
all_frames: true,
run_at: "document_start",
},
{
matches: ["*://mochi.test/*"],
match_origin_as_fallback: true,
js: ["moaf_true.js"],
all_frames: true,
run_at: "document_start",
},
],
},
files: {
"moaf_false.js": () => {
if (location.protocol == "blob:") {
const { name } = browser.runtime.getManifest();
browser.test.log(`moaf_false.js: Ran ${name} at ${document.URL}`);
browser.test.sendMessage(name + ":moaf_false:" + document.URL);
}
},
"moaf_true.js": () => {
if (location.protocol == "blob:") {
const { name } = browser.runtime.getManifest();
browser.test.log(`moaf_true.js: Ran ${name} at ${document.URL}`);
browser.test.sendMessage(name + ":moaf_true:" + document.URL);
}
},
},
});
}
function createBlobURL() {
function blobScript() {
window.onload = () => {
console.log(`Web page ${document.URL} loaded at origin ${origin}`);
parent.postMessage(document.URL, "*");
};
}
const html = `<!DOCTYPE html><script>(${blobScript})()<\/script>`;
return URL.createObjectURL(new Blob([html], { type: "text/html" }));
}
async function createFrameAndAwaitLoad(blobUrl, sandboxed) {
let { promise, resolve } = Promise.withResolvers();
let f = document.createElement("iframe");
f.src = blobUrl;
if (sandboxed) {
f.sandbox = "allow-scripts";
}
function onmessage(event) {
if (event.source === f.contentWindow) {
is(event.data, blobUrl, "Got message from frame");
is(event.origin, sandboxed ? "null" : origin, "Frame has correct origin");
resolve();
}
}
window.addEventListener("message", onmessage);
document.body.append(f);
await promise;
window.removeEventListener("message", onmessage);
f.remove();
}
async function test_contentscript_at_blob(legacy) {
// TODO bug 1899134: Drop the pref and legacy=true case.
await SpecialPowers.pushPrefEnv({
set: [["extensions.script_blob_without_match_origin_as_fallback", legacy]],
});
const extension2 = loadTestExtension({ manifest_version: 2 });
const extension3 = loadTestExtension({ manifest_version: 3 });
await extension2.startup();
await extension3.startup();
const blobUrlSameOrigin = createBlobURL();
info(`Expecting content scripts at blobUrlSameOrigin:${blobUrlSameOrigin}`);
await createFrameAndAwaitLoad(blobUrlSameOrigin, /* sandboxed */ false);
await Promise.all([
await extension2.awaitMessage("MV2:moaf_true:" + blobUrlSameOrigin),
await extension3.awaitMessage("MV3:moaf_true:" + blobUrlSameOrigin),
]);
if (legacy) {
await extension2.awaitMessage("MV2:moaf_false:" + blobUrlSameOrigin);
}
// MV3:moaf_false should never be observed because match_origin_as_fallback
// is required in order to execute content scripts in blob:-URLs.
const blobUrlNullOrigin = createBlobURL();
info(`Expecting content scripts at blobUrlNullOrigin:${blobUrlNullOrigin}`);
await createFrameAndAwaitLoad(blobUrlNullOrigin, /* sandboxed */ true);
await Promise.all([
await extension2.awaitMessage("MV2:moaf_true:" + blobUrlNullOrigin),
await extension3.awaitMessage("MV3:moaf_true:" + blobUrlNullOrigin),
]);
if (legacy) {
await extension2.awaitMessage("MV2:moaf_false:" + blobUrlNullOrigin);
}
await extension2.unload();
await extension3.unload();
await SpecialPowers.popPrefEnv();
}
add_task(async function test_contentscript_at_blob_default() {
await test_contentscript_at_blob(/* legacy */ false);
});
// Exactly the same as test_contentscript_at_blob_default, except
// manifest_version 2 also run at blob: when match_origin_as_fallback is false.
add_task(async function test_contentscript_at_blob_legacy_behavior() {
await test_contentscript_at_blob(/* legacy */ true);
});
</script>
</body>
</html>

View file

@ -408,10 +408,14 @@ add_task(async function test_no_preload_at_blob_url_iframe() {
manifest: {
content_scripts: [
{
// Note: match_origin_as_fallback is supposed to only work when
// "matches" has a wildcard path. In our implementation, blob:-URLs
// have a principal URL that may include a path rather than just the
// origin, so we can match blob:-URLs created from specific paths.
// This behavior is NOT documented, but relied upon for convenience
// here.
matches: ["*://example.com/dummy?with_blob_url"],
// Note: we currently match at blob:-URLs even without
// match_origin_as_fallback (or even match_about_blank). In Chrome,
// blob:-URLs can only be matched with match_origin_as_fallback:true.
match_origin_as_fallback: true,
all_frames: true,
js: ["done.js"],
run_at: "document_end",

View file

@ -2007,6 +2007,7 @@ STATIC_ATOMS = [
Atom("tabs", "tabs"),
Atom("webRequestBlocking", "webRequestBlocking"),
Atom("webRequestFilterResponse_serviceWorkerScript", "webRequestFilterResponse.serviceWorkerScript"),
Atom("blob", "blob"),
Atom("http", "http"),
Atom("https", "https"),
Atom("view_source", "view-source"),