From 0dec35cac7912868d75d090b413442efc79b8d98 Mon Sep 17 00:00:00 2001 From: Tomislav Jovanovic Date: Thu, 24 Mar 2022 23:41:01 +0000 Subject: [PATCH] Bug 1745819 - Require origin permission for content scripts in mv3 r=robwu Differential Revision: https://phabricator.services.mozilla.com/D141557 --- dom/chrome-webidl/WebExtensionContentScript.webidl | 17 +- .../extensions/WebExtensionContentScript.h | 6 +- .../components/extensions/WebExtensionPolicy.cpp | 15 +- .../test_ext_scripting_contentScripts.html | 2 + .../xpcshell/test_WebExtensionContentScript.js | 216 ++++++++++++++++----- .../test_ext_contentscript_dynamic_registration.js | 3 + .../test_ext_contentscript_triggeringPrincipal.js | 4 + .../xpcshell/test_ext_scripting_contentScripts.js | 3 + .../test_ext_scripting_contentScripts_css.js | 3 + .../test_ext_scripting_contentScripts_file.js | 3 + .../test_ext_scripting_updateContentScripts.js | 3 + .../test_ext_web_accessible_resources_matches.js | 3 + 12 files changed, 214 insertions(+), 64 deletions(-) diff --git a/dom/chrome-webidl/WebExtensionContentScript.webidl b/dom/chrome-webidl/WebExtensionContentScript.webidl index 27e2b46156cf..a97a535e9bc8 100644 --- a/dom/chrome-webidl/WebExtensionContentScript.webidl +++ b/dom/chrome-webidl/WebExtensionContentScript.webidl @@ -21,13 +21,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. */ @@ -40,6 +33,14 @@ interface MozDocumentMatcher { 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 * matches one of the match patterns, such as about:blank or about:srcdoc. @@ -102,6 +103,8 @@ interface MozDocumentMatcher { dictionary MozDocumentMatcherInit { boolean allFrames = false; + boolean checkPermissions = false; + sequence? originAttributesPatterns = null; boolean matchAboutBlank = false; diff --git a/toolkit/components/extensions/WebExtensionContentScript.h b/toolkit/components/extensions/WebExtensionContentScript.h index 34a5ab5b5289..046d141020e6 100644 --- a/toolkit/components/extensions/WebExtensionContentScript.h +++ b/toolkit/components/extensions/WebExtensionContentScript.h @@ -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 mExcludeGlobs; bool mAllFrames; + bool mCheckPermissions; Nullable mFrameID; bool mMatchAboutBlank; Nullable> mOriginAttributesPatterns; diff --git a/toolkit/components/extensions/WebExtensionPolicy.cpp b/toolkit/components/extensions/WebExtensionPolicy.cpp index aaa3074e8791..4c83d6d2d7af 100644 --- a/toolkit/components/extensions/WebExtensionPolicy.cpp +++ b/toolkit/components/extensions/WebExtensionPolicy.cpp @@ -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; } diff --git a/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html b/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html index b19db8d67efa..19863a361f65 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_scripting_contentScripts.html @@ -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, }); }; diff --git a/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js index 78d61d4b2979..2427c3c1be40 100644 --- a/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js +++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js @@ -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", - - allowedOrigins: new MatchPatternSet([]), - localizeCallback() {}, -}); +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", + + 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" ); +} + +add_task(function test_WebExtensionContentScript_urls_mv2() { + return test_url_matching({ manifestVersion: 2, expectMatches: true }); }); -async function loadURL(url) { - let requests = new Map(); +add_task(function test_WebExtensionContentScript_urls_mv2_checkPermissions() { + return test_url_matching({ + manifestVersion: 2, + checkPermissions: true, + expectMatches: false, + }); +}); - function requestObserver(request) { - request.QueryInterface(Ci.nsIChannel); - if (request.isDocument) { - requests.set(request.name, request); - } - } +add_task(function test_WebExtensionContentScript_urls_mv2_with_permissions() { + return test_url_matching({ + manifestVersion: 2, + checkPermissions: true, + allowedOrigins: [""], + expectMatches: true, + }); +}); - Services.obs.addObserver(requestObserver, "http-on-examine-response"); +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, + }); +}); - let contentPage = await ExtensionTestUtils.loadContentPage(url); +add_task(function test_WebExtensionContentScript_mv3_all_urls() { + return test_url_matching({ + manifestVersion: 3, + allowedOrigins: [""], + expectMatches: true, + }); +}); + +add_task(function test_WebExtensionContentScript_mv3_wildcards() { + return test_url_matching({ + manifestVersion: 3, + allowedOrigins: ["*://*.foo.com/*", "*://*.bar.com/*"], + expectMatches: true, + }); +}); - Services.obs.removeObserver(requestObserver, "http-on-examine-response"); +add_task(function test_WebExtensionContentScript_mv3_specific() { + return test_url_matching({ + manifestVersion: 3, + allowedOrigins: ["http://www.foo.com/*", "https://bar.com/*"], + expectMatches: true, + }); +}); - return { contentPage, requests }; -} +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([""]), + localizeCallback() {}, + }); + let contentScript = new WebExtensionContentScript(policy, { + checkPermissions: true, + matches: new MatchPatternSet([""]), + }); + + // 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` + ); + } +}); -add_task(async function test_WebExtensinonContentScript_frame_matching() { +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; + await contentPage.close(); +} + +add_task(function test_WebExtensionContentScript_frames_mv2() { + return test_frame_matching({ + manifestVersion: 2, + expectMatches: true, }); +}); - for (let [i, test] of tests.entries()) { - for (let [frame, url] of Object.entries(urls)) { - let should = test[frame] ? "should" : "should not"; +add_task(function test_WebExtensionContentScript_frames_mv3() { + return test_frame_matching({ + manifestVersion: 3, + expectMatches: false, + }); +}); - if (url.startsWith("http")) { - let request = requests.get(url); +add_task(function test_WebExtensionContentScript_frames_mv3_all_urls() { + return test_frame_matching({ + manifestVersion: 3, + allowedOrigins: [""], + expectMatches: true, + }); +}); - equal( - test.script.matchesLoadInfo(request.URI, request.loadInfo), - test[frame], - `Script ${i} ${should} match the request LoadInfo for ${frame} frame` - ); - } - } - } +add_task(function test_WebExtensionContentScript_frames_mv3_wildcards() { + return test_frame_matching({ + manifestVersion: 3, + allowedOrigins: ["*://*.example.com/*"], + expectMatches: true, + }); +}); - await contentPage.close(); +add_task(function test_WebExtensionContentScript_frames_mv3_specific() { + return test_frame_matching({ + manifestVersion: 3, + allowedOrigins: ["http://example.com/*"], + expectMatches: true, + }); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js index aa963d1cb372..c62812ef9d69 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_dynamic_registration.js @@ -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: [""], + granted_host_permissions: true, }, async background() { const script = { diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js index f5df8e61d2f5..1ca58e98a525 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js @@ -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(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js index 79e9a67c6439..464c6bd31dd0 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts.js @@ -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, }); }; diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js index 63ee0f0404bf..c62693ca4b9c 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_css.js @@ -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, }); }; diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js index 9cb7aaf856f0..3c806439ce63 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_contentScripts_file.js @@ -15,8 +15,11 @@ const makeExtension = ({ manifest: manifestProps, ...otherProps }) => { manifest: { manifest_version: 3, permissions: ["scripting"], + host_permissions: [""], + granted_host_permissions: true, ...manifestProps, }, + temporarilyInstalled: true, ...otherProps, }); }; diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js index 8b872d25bbf9..9d3bf1576c98 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_scripting_updateContentScripts.js @@ -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, }); }; diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_web_accessible_resources_matches.js b/toolkit/components/extensions/test/xpcshell/test_ext_web_accessible_resources_matches.js index 832e9966577a..b46d5ae4c212 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_web_accessible_resources_matches.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_web_accessible_resources_matches.js @@ -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, -- 2.11.4.GIT