Bug 1745819 - Require origin permission for content scripts in mv3 r=robwu

Differential Revision: https://phabricator.services.mozilla.com/D141557
This commit is contained in:
Tomislav Jovanovic 2022-03-24 23:41:01 +00:00
Родитель 64d31d44db
Коммит 0dec35cac7
12 изменённых файлов: 223 добавлений и 73 удалений

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

@ -20,13 +20,6 @@ interface MozDocumentMatcher {
*/
boolean matchesURI(URI uri);
/**
* Returns true if the the given URI and LoadInfo objects match.
* This should be used to determine whether to begin pre-loading a content
* script based on network events.
*/
boolean matchesLoadInfo(URI uri, LoadInfo loadInfo);
/**
* Returns true if the given window matches. This should be used
* to determine whether to run a script in a window at load time.
@ -39,6 +32,14 @@ interface MozDocumentMatcher {
[Constant]
readonly attribute boolean allFrames;
/**
* If we can't check extension has permissions to access the URI upfront,
* set the flag to perform the origin check at runtime, upon matching.
* This is always true in MV3, where host permissions are optional.
*/
[Constant]
readonly attribute boolean checkPermissions;
/**
* If true, this (misleadingly-named, but inherited from Chrome) attribute
* causes us to match frames with URLs which inherit a principal that
@ -102,6 +103,8 @@ interface MozDocumentMatcher {
dictionary MozDocumentMatcherInit {
boolean allFrames = false;
boolean checkPermissions = false;
sequence<OriginAttributesPatternDictionary>? originAttributesPatterns = null;
boolean matchAboutBlank = false;

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

@ -115,10 +115,6 @@ class MozDocumentMatcher : public nsISupports, public nsWrapperCache {
bool Matches(const DocInfo& aDoc) const;
bool MatchesURI(const URLInfo& aURL) const;
bool MatchesLoadInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo) const {
return Matches({aURL, aLoadInfo});
}
bool MatchesWindowGlobal(dom::WindowGlobalChild& aWindow) const;
WebExtensionPolicy* GetExtension() { return mExtension; }
@ -127,6 +123,7 @@ class MozDocumentMatcher : public nsISupports, public nsWrapperCache {
const WebExtensionPolicy* Extension() const { return mExtension; }
bool AllFrames() const { return mAllFrames; }
bool CheckPermissions() const { return mCheckPermissions; }
bool MatchAboutBlank() const { return mMatchAboutBlank; }
MatchPatternSet* Matches() { return mMatches; }
@ -173,6 +170,7 @@ class MozDocumentMatcher : public nsISupports, public nsWrapperCache {
Nullable<MatchGlobSet> mExcludeGlobs;
bool mAllFrames;
bool mCheckPermissions;
Nullable<uint64_t> mFrameID;
bool mMatchAboutBlank;
Nullable<dom::Sequence<OriginAttributesPattern>> mOriginAttributesPatterns;

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

@ -634,6 +634,7 @@ MozDocumentMatcher::MozDocumentMatcher(GlobalObject& aGlobal,
: mHasActiveTabPermission(aInit.mHasActiveTabPermission),
mRestricted(aRestricted),
mAllFrames(aInit.mAllFrames),
mCheckPermissions(aInit.mCheckPermissions),
mFrameID(aInit.mFrameID),
mMatchAboutBlank(aInit.mMatchAboutBlank) {
MatchPatternOptions options;
@ -690,6 +691,11 @@ WebExtensionContentScript::WebExtensionContentScript(
mCssPaths.Assign(aInit.mCssPaths);
mJsPaths.Assign(aInit.mJsPaths);
mExtension = &aExtension;
// Origin permissions are optional in mv3, so always check them at runtime.
if (mExtension->ManifestVersion() >= 3) {
mCheckPermissions = true;
}
}
bool MozDocumentMatcher::Matches(const DocInfo& aDoc) const {
@ -738,7 +744,7 @@ bool MozDocumentMatcher::Matches(const DocInfo& aDoc) const {
return true;
}
if (mRestricted && mExtension->IsRestrictedDoc(aDoc)) {
if (mRestricted && mExtension && mExtension->IsRestrictedDoc(aDoc)) {
return false;
}
@ -752,6 +758,8 @@ bool MozDocumentMatcher::Matches(const DocInfo& aDoc) const {
}
bool MozDocumentMatcher::MatchesURI(const URLInfo& aURL) const {
MOZ_ASSERT(!mRestricted && !mCheckPermissions || mExtension);
if (!mMatches->Matches(aURL)) {
return false;
}
@ -772,6 +780,11 @@ bool MozDocumentMatcher::MatchesURI(const URLInfo& aURL) const {
return false;
}
if (mCheckPermissions &&
!mExtension->CanAccessURI(aURL, false, false, true)) {
return false;
}
return true;
}

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

@ -30,8 +30,10 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
// Used in `file_contains_iframe.html`
"*://example.org/",
],
granted_host_permissions: true,
...manifestProps,
},
temporarilyInstalled: true,
...otherProps,
});
};

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

@ -7,18 +7,26 @@ const { newURI } = Services.io;
const server = createHttpServer({ hosts: ["example.com"] });
server.registerDirectory("/data/", do_get_file("data"));
let policy = new WebExtensionPolicy({
id: "foo@bar.baz",
mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2",
baseURL: "file:///foo",
async function test_url_matching({
manifestVersion = 2,
allowedOrigins = [],
checkPermissions,
expectMatches,
}) {
let policy = new WebExtensionPolicy({
id: "foo@bar.baz",
mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2",
baseURL: "file:///foo",
allowedOrigins: new MatchPatternSet([]),
localizeCallback() {},
});
manifestVersion,
allowedOrigins: new MatchPatternSet(allowedOrigins),
localizeCallback() {},
});
add_task(async function test_WebExtensinonContentScript_url_matching() {
let contentScript = new WebExtensionContentScript(policy, {
matches: new MatchPatternSet(["http://foo.com/bar", "*://bar.com/baz/*"]),
checkPermissions,
matches: new MatchPatternSet(["http://*.foo.com/bar", "*://bar.com/baz/*"]),
excludeMatches: new MatchPatternSet(["*://bar.com/baz/quux"]),
@ -29,14 +37,16 @@ add_task(async function test_WebExtensinonContentScript_url_matching() {
excludeGlobs: ["*glorg*"].map(glob => new MatchGlob(glob)),
});
ok(
contentScript.matchesURI(newURI("http://foo.com/bar")),
"Simple matches include should match"
equal(
expectMatches,
contentScript.matchesURI(newURI("http://www.foo.com/bar")),
`Simple matches include should ${expectMatches ? "" : "not "} match.`
);
ok(
equal(
expectMatches,
contentScript.matchesURI(newURI("https://bar.com/baz/xflergx")),
"Simple matches include should match"
`Simple matches include should ${expectMatches ? "" : "not "} match.`
);
ok(
@ -53,28 +63,112 @@ add_task(async function test_WebExtensinonContentScript_url_matching() {
!contentScript.matchesURI(newURI("https://bar.com/baz/xflergxglorgx")),
"Excluded match glob should not match"
);
});
async function loadURL(url) {
let requests = new Map();
function requestObserver(request) {
request.QueryInterface(Ci.nsIChannel);
if (request.isDocument) {
requests.set(request.name, request);
}
}
Services.obs.addObserver(requestObserver, "http-on-examine-response");
let contentPage = await ExtensionTestUtils.loadContentPage(url);
Services.obs.removeObserver(requestObserver, "http-on-examine-response");
return { contentPage, requests };
}
add_task(async function test_WebExtensinonContentScript_frame_matching() {
add_task(function test_WebExtensionContentScript_urls_mv2() {
return test_url_matching({ manifestVersion: 2, expectMatches: true });
});
add_task(function test_WebExtensionContentScript_urls_mv2_checkPermissions() {
return test_url_matching({
manifestVersion: 2,
checkPermissions: true,
expectMatches: false,
});
});
add_task(function test_WebExtensionContentScript_urls_mv2_with_permissions() {
return test_url_matching({
manifestVersion: 2,
checkPermissions: true,
allowedOrigins: ["<all_urls>"],
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_urls_mv3() {
// checkPermissions ignored here because it's forced for MV3.
return test_url_matching({
manifestVersion: 3,
checkPermissions: false,
expectMatches: false,
});
});
add_task(function test_WebExtensionContentScript_mv3_all_urls() {
return test_url_matching({
manifestVersion: 3,
allowedOrigins: ["<all_urls>"],
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_mv3_wildcards() {
return test_url_matching({
manifestVersion: 3,
allowedOrigins: ["*://*.foo.com/*", "*://*.bar.com/*"],
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_mv3_specific() {
return test_url_matching({
manifestVersion: 3,
allowedOrigins: ["http://www.foo.com/*", "https://bar.com/*"],
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_restricted() {
let tests = [
{
manifestVersion: 2,
permissions: [],
expect: false,
},
{
manifestVersion: 2,
permissions: ["mozillaAddons"],
expect: true,
},
{
manifestVersion: 3,
permissions: [],
expect: false,
},
{
manifestVersion: 3,
permissions: ["mozillaAddons"],
expect: true,
},
];
for (let { manifestVersion, permissions, expect } of tests) {
let policy = new WebExtensionPolicy({
id: "foo@bar.baz",
mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2",
baseURL: "file:///foo",
manifestVersion,
permissions,
allowedOrigins: new MatchPatternSet(["<all_urls>"]),
localizeCallback() {},
});
let contentScript = new WebExtensionContentScript(policy, {
checkPermissions: true,
matches: new MatchPatternSet(["<all_urls>"]),
});
// AMO is on the extensions.webextensions.restrictedDomains list.
equal(
expect,
contentScript.matchesURI(newURI("https://addons.mozilla.org/foo")),
`Expect extension with [${permissions}] to ${expect ? "" : "not"} match`
);
}
});
async function test_frame_matching(meta) {
if (AppConstants.platform == "linux") {
// The windowless browser currently does not load correctly on Linux on
// infra.
@ -89,7 +183,7 @@ add_task(async function test_WebExtensinonContentScript_frame_matching() {
aboutBlank: "about:blank",
};
let { contentPage, requests } = await loadURL(urls.topLevel);
let contentPage = await ExtensionTestUtils.loadContentPage(urls.topLevel);
let tests = [
{
@ -149,7 +243,9 @@ add_task(async function test_WebExtensinonContentScript_frame_matching() {
];
// matchesWindowGlobal tests against content frames
await contentPage.spawn({ tests, urls }, args => {
await contentPage.spawn({ tests, urls, meta }, args => {
let { manifestVersion = 2, allowedOrigins = [], expectMatches } = args.meta;
this.windows = new Map();
this.windows.set(this.content.location.href, this.content);
for (let c of Array.from(this.content.frames)) {
@ -160,7 +256,8 @@ add_task(async function test_WebExtensinonContentScript_frame_matching() {
mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2",
baseURL: "file:///foo",
allowedOrigins: new MatchPatternSet([]),
manifestVersion,
allowedOrigins: new MatchPatternSet(allowedOrigins),
localizeCallback() {},
});
@ -175,35 +272,50 @@ add_task(async function test_WebExtensinonContentScript_frame_matching() {
let wgc = this.windows.get(url).windowGlobalChild;
Assert.equal(
test.script.matchesWindowGlobal(wgc),
test[frame],
test[frame] && expectMatches,
`Script ${i} ${should} match the ${frame} frame`
);
}
}
});
// Parent tests against loadInfo
tests = tests.map(t => {
t.contentScript.matches = new MatchPatternSet(t.matches);
t.script = new WebExtensionContentScript(policy, t.contentScript);
return t;
});
for (let [i, test] of tests.entries()) {
for (let [frame, url] of Object.entries(urls)) {
let should = test[frame] ? "should" : "should not";
if (url.startsWith("http")) {
let request = requests.get(url);
equal(
test.script.matchesLoadInfo(request.URI, request.loadInfo),
test[frame],
`Script ${i} ${should} match the request LoadInfo for ${frame} frame`
);
}
}
}
await contentPage.close();
}
add_task(function test_WebExtensionContentScript_frames_mv2() {
return test_frame_matching({
manifestVersion: 2,
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_frames_mv3() {
return test_frame_matching({
manifestVersion: 3,
expectMatches: false,
});
});
add_task(function test_WebExtensionContentScript_frames_mv3_all_urls() {
return test_frame_matching({
manifestVersion: 3,
allowedOrigins: ["<all_urls>"],
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_frames_mv3_wildcards() {
return test_frame_matching({
manifestVersion: 3,
allowedOrigins: ["*://*.example.com/*"],
expectMatches: true,
});
});
add_task(function test_WebExtensionContentScript_frames_mv3_specific() {
return test_frame_matching({
manifestVersion: 3,
allowedOrigins: ["http://example.com/*"],
expectMatches: true,
});
});

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

@ -21,6 +21,7 @@ const makeExtension = ({ background, manifest }) => {
permissions:
manifest.manifest_version === 3 ? ["scripting"] : ["http://*/*/*.html"],
},
temporarilyInstalled: true,
background,
files: {
"script.js": () => {
@ -132,6 +133,8 @@ add_task(
let extension = makeExtension({
manifest: {
manifest_version: 3,
host_permissions: ["<all_urls>"],
granted_host_permissions: true,
},
async background() {
const script = {

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

@ -1344,8 +1344,12 @@ add_task(async function test_extension_contentscript_csp() {
manifest: {
...EXTENSION_DATA.manifest,
manifest_version: 3,
host_permissions: ["http://example.com/*"],
granted_host_permissions: true,
},
temporarilyInstalled: true,
};
let extension = ExtensionTestUtils.loadExtension(data);
await extension.startup();

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

@ -16,8 +16,11 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
manifest: {
manifest_version: 3,
permissions: ["scripting"],
host_permissions: ["http://localhost/*"],
granted_host_permissions: true,
...manifestProps,
},
temporarilyInstalled: true,
...otherProps,
});
};

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

@ -16,8 +16,11 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
manifest: {
manifest_version: 3,
permissions: ["scripting"],
host_permissions: ["http://localhost/*"],
granted_host_permissions: true,
...manifestProps,
},
temporarilyInstalled: true,
...otherProps,
});
};

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

@ -15,8 +15,11 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
manifest: {
manifest_version: 3,
permissions: ["scripting"],
host_permissions: ["<all_urls>"],
granted_host_permissions: true,
...manifestProps,
},
temporarilyInstalled: true,
...otherProps,
});
};

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

@ -16,8 +16,11 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
manifest: {
manifest_version: 3,
permissions: ["scripting"],
host_permissions: ["http://localhost/*"],
granted_host_permissions: true,
...manifestProps,
},
temporarilyInstalled: true,
...otherProps,
});
};

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

@ -78,6 +78,8 @@ add_task(async function test_web_accessible_resources() {
run_at: "document_idle",
},
],
host_permissions: ["http://example.com/*", "http://example.org/*"],
granted_host_permissions: true,
web_accessible_resources: [
{
@ -86,6 +88,7 @@ add_task(async function test_web_accessible_resources() {
},
],
},
temporarilyInstalled: true,
files: {
"content_script.js": contentScript,