author | Kris Maglione <maglione.k@gmail.com> |
Wed, 09 May 2018 18:55:59 -0700 | |
changeset 419410 | 364fa52d097e335146f94cbc98238c77b7789d61 |
parent 419409 | 8c259ed0e0d1cecf8fdc6092b8714e880e3169d5 |
child 419411 | a57842c1d43846ff12687c85ec4138e41b5d2c60 |
push id | 103528 |
push user | maglione.k@gmail.com |
push date | Tue, 22 May 2018 22:59:41 +0000 |
treeherder | mozilla-inbound@364fa52d097e [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | zombie |
bugs | 1456485 |
milestone | 62.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -505,16 +505,17 @@ class ExtensionData { permissions: newPermissions.permissions.filter(perm => !oldPermissions.permissions.includes(perm)), }; } canUseExperiment(manifest) { return this.experimentsAllowed && manifest.experiment_apis; } + // eslint-disable-next-line complexity async parseManifest() { let [manifest] = await Promise.all([ this.readJSON("manifest.json"), Management.lazyInit(), ]); this.manifest = manifest; this.rawManifest = manifest; @@ -591,31 +592,38 @@ class ExtensionData { originPermissions, permissions, schemaURLs: null, type: this.type, webAccessibleResources, }; if (this.type === "extension") { + let restrictSchemes = !(this.isPrivileged && manifest.permissions.includes("mozillaAddons")); + for (let perm of manifest.permissions) { if (perm === "geckoProfiler" && !this.isPrivileged) { const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", ""); if (!acceptedExtensions.split(",").includes(id)) { this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler."); continue; } } let type = classifyPermission(perm); if (type.origin) { - let matcher = new MatchPattern(perm, {ignorePath: true}); + try { + let matcher = new MatchPattern(perm, {restrictSchemes, ignorePath: true}); - perm = matcher.pattern; - originPermissions.add(perm); + perm = matcher.pattern; + originPermissions.add(perm); + } catch (e) { + this.manifestWarning(`Invalid host permission: ${perm}`); + continue; + } } else if (type.api) { apiNames.add(type.api); } permissions.add(perm); } if (this.id) { @@ -795,21 +803,38 @@ class ExtensionData { this.type = manifestData.type; this.modules = manifestData.modules; this.apiManager = this.getAPIManager(); await this.apiManager.lazyInit(); this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res)); - this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions); + this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions, {restrictSchemes: !this.hasPermission("mozillaAddons")}); return this.manifest; } + hasPermission(perm, includeOptional = false) { + let manifest_ = "manifest:"; + if (perm.startsWith(manifest_)) { + return this.manifest[perm.substr(manifest_.length)] != null; + } + + if (this.permissions.has(perm)) { + return true; + } + + if (includeOptional && this.manifest.optional_permissions.includes(perm)) { + return true; + } + + return false; + } + getAPIManager() { let apiManagers = [Management]; for (let id of this.dependencies) { let policy = WebExtensionPolicy.getByID(id); if (policy) { apiManagers.push(policy.extension.experimentAPIManager); } @@ -1269,17 +1294,17 @@ class Extension extends ExtensionData { for (let perm of permissions.permissions) { this.permissions.add(perm); } if (permissions.origins.length > 0) { let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); this.whiteListedHosts = new MatchPatternSet(new Set([...patterns, ...permissions.origins]), - {ignorePath: true}); + {restrictSchemes: !this.hasPermission("mozillaAddons"), ignorePath: true}); } this.policy.permissions = Array.from(this.permissions); this.policy.allowedOrigins = this.whiteListedHosts; this.cachePermissions(); }); @@ -1838,41 +1863,24 @@ class Extension extends ExtensionData { } observe(subject, topic, data) { if (topic === "xpcom-shutdown") { this.cleanupGeneratedFile(); } } - hasPermission(perm, includeOptional = false) { - let manifest_ = "manifest:"; - if (perm.startsWith(manifest_)) { - return this.manifest[perm.substr(manifest_.length)] != null; - } - - if (this.permissions.has(perm)) { - return true; - } - - if (includeOptional && this.manifest.optional_permissions.includes(perm)) { - return true; - } - - return false; - } - get name() { return this.manifest.name; } get optionalOrigins() { if (this._optionalOrigins == null) { let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin); - this._optionalOrigins = new MatchPatternSet(origins, {ignorePath: true}); + this._optionalOrigins = new MatchPatternSet(origins, {restrictSchemes: !this.hasPermission("mozillaAddons"), ignorePath: true}); } return this._optionalOrigins; } } class Dictionary extends ExtensionData { constructor(addonData, startupReason) { super(addonData.resourceURI);
--- a/toolkit/components/extensions/ExtensionChild.jsm +++ b/toolkit/components/extensions/ExtensionChild.jsm @@ -591,21 +591,24 @@ class BrowserExtensionContent extends Ev this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); defineLazyGetter(this, "scripts", () => { return data.contentScripts.map(scriptData => new ExtensionContent.Script(this, scriptData)); }); this.webAccessibleResources = data.webAccessibleResources.map(res => new MatchGlob(res)); - this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts, {ignorePath: true}); this.permissions = data.permissions; this.optionalPermissions = data.optionalPermissions; this.principal = data.principal; + let restrictSchemes = !this.hasPermission("mozillaAddons"); + + this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts, {restrictSchemes, ignorePath: true}); + this.apiManager = this.getAPIManager(); this.localeData = new LocaleData(data.localeData); this.manifest = data.manifest; this.baseURL = data.baseURL; this.baseURI = Services.io.newURI(data.baseURL); @@ -622,17 +625,17 @@ class BrowserExtensionContent extends Ev this.permissions.add(perm); } } if (permissions.origins.length > 0) { let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins], - {ignorePath: true}); + {restrictSchemes, ignorePath: true}); } if (this.policy) { this.policy.permissions = Array.from(this.permissions); this.policy.allowedOrigins = this.whiteListedHosts; } });
--- a/toolkit/components/extensions/WebExtensionContentScript.h +++ b/toolkit/components/extensions/WebExtensionContentScript.h @@ -156,16 +156,17 @@ protected: WebExtensionContentScript(WebExtensionPolicy& aExtension, const ContentScriptInit& aInit, ErrorResult& aRv); private: RefPtr<WebExtensionPolicy> mExtension; bool mHasActiveTabPermission; + bool mRestricted; RefPtr<MatchPatternSet> mMatches; RefPtr<MatchPatternSet> mExcludeMatches; Nullable<MatchGlobSet> mIncludeGlobs; Nullable<MatchGlobSet> mExcludeGlobs; nsTArray<nsString> mCssPaths;
--- a/toolkit/components/extensions/WebExtensionPolicy.cpp +++ b/toolkit/components/extensions/WebExtensionPolicy.cpp @@ -444,16 +444,17 @@ WebExtensionContentScript::Constructor(G return script.forget(); } WebExtensionContentScript::WebExtensionContentScript(WebExtensionPolicy& aExtension, const ContentScriptInit& aInit, ErrorResult& aRv) : mExtension(&aExtension) , mHasActiveTabPermission(aInit.mHasActiveTabPermission) + , mRestricted(!aExtension.HasPermission(nsGkAtoms::mozillaAddons)) , mMatches(aInit.mMatches) , mExcludeMatches(aInit.mExcludeMatches) , mCssPaths(aInit.mCssPaths) , mJsPaths(aInit.mJsPaths) , mRunAt(aInit.mRunAt) , mAllFrames(aInit.mAllFrames) , mFrameID(aInit.mFrameID) , mMatchAboutBlank(aInit.mMatchAboutBlank) @@ -488,17 +489,17 @@ WebExtensionContentScript::Matches(const // matchAboutBlank is true and it has the null principal. In all other // cases, we test the URL of the principal that it inherits. if (mMatchAboutBlank && aDoc.IsTopLevel() && aDoc.URL().Spec().EqualsLiteral("about:blank") && aDoc.Principal() && aDoc.Principal()->GetIsNullPrincipal()) { return true; } - if (mExtension->IsRestrictedDoc(aDoc)) { + if (mRestricted && mExtension->IsRestrictedDoc(aDoc)) { return false; } auto& urlinfo = aDoc.PrincipalURL(); if (mHasActiveTabPermission && aDoc.ShouldMatchActiveTabPermission() && MatchPattern::MatchesAllURLs(urlinfo)) { return true; } @@ -520,17 +521,17 @@ WebExtensionContentScript::MatchesURI(co if (!mIncludeGlobs.IsNull() && !mIncludeGlobs.Value().Matches(aURL.Spec())) { return false; } if (!mExcludeGlobs.IsNull() && mExcludeGlobs.Value().Matches(aURL.Spec())) { return false; } - if (mExtension->IsRestrictedURI(aURL)) { + if (mRestricted && mExtension->IsRestrictedURI(aURL)) { return false; } return true; } JSObject*
--- a/toolkit/components/extensions/extension-process-script.js +++ b/toolkit/components/extensions/extension-process-script.js @@ -30,26 +30,35 @@ const { } = ExtensionUtils; // We need to avoid touching Services.appinfo here in order to prevent // the wrong version from being cached during xpcshell test startup. // eslint-disable-next-line mozilla/use-services const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT; -function parseScriptOptions(options) { +function tryMatchPatternSet(patterns, options) { + try { + return new MatchPatternSet(patterns, options); + } catch (e) { + Cu.reportError(e); + return new MatchPatternSet([]); + } +} + +function parseScriptOptions(options, restrictSchemes = true) { return { allFrames: options.all_frames, matchAboutBlank: options.match_about_blank, frameID: options.frame_id, runAt: options.run_at, hasActiveTabPermission: options.hasActiveTabPermission, - matches: new MatchPatternSet(options.matches), - excludeMatches: new MatchPatternSet(options.exclude_matches || []), + matches: tryMatchPatternSet(options.matches, {restrictSchemes}), + excludeMatches: tryMatchPatternSet(options.exclude_matches || [], {restrictSchemes}), includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)), excludeGlobs: options.exclude_globs && options.exclude_globs.map(glob => new MatchGlob(glob)), jsPaths: options.js || [], cssPaths: options.css || [], }; } @@ -129,17 +138,17 @@ class ExtensionGlobal { switch (messageName) { case "Extension:Capture": return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options); case "Extension:DetectLanguage": return ExtensionContent.handleDetectLanguage(this.global, target); case "Extension:Execute": let policy = WebExtensionPolicy.getByID(recipient.extensionId); - let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options)); + let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options, !policy.hasPermission("mozillaAddons"))); Object.assign(matcher, { wantReturnValue: data.options.wantReturnValue, removeCSS: data.options.remove_css, cssOrigin: data.options.css_origin, jsCode: data.options.jsCode, }); @@ -319,16 +328,18 @@ ExtensionManager = { webAccessibleResources = extension.webAccessibleResources; } else { // We have serialized extension data; localizeCallback = str => extensions.get(policy).localize(str); allowedOrigins = new MatchPatternSet(extension.whiteListedHosts); webAccessibleResources = extension.webAccessibleResources.map(host => new MatchGlob(host)); } + let restrictSchemes = !extension.permissions.has("mozillaAddons"); + policy = new WebExtensionPolicy({ id: extension.id, mozExtensionHostname: extension.uuid, name: extension.name, baseURL: extension.resourceURL, permissions: Array.from(extension.permissions), allowedOrigins, @@ -336,29 +347,29 @@ ExtensionManager = { contentSecurityPolicy: extension.manifest.content_security_policy, localizeCallback, backgroundScripts: (extension.manifest.background && extension.manifest.background.scripts), - contentScripts: extension.contentScripts.map(parseScriptOptions), + contentScripts: extension.contentScripts.map(script => parseScriptOptions(script, restrictSchemes)), }); policy.debugName = `${JSON.stringify(policy.name)} (ID: ${policy.id}, ${policy.getURL()})`; // Register any existent dynamically registered content script for the extension // when a content process is started for the first time (which also cover // a content process that crashed and it has been recreated). const registeredContentScripts = this.registeredContentScripts.get(policy); if (extension.registeredContentScripts) { for (let [scriptId, options] of extension.registeredContentScripts) { - const parsedOptions = parseScriptOptions(options); + const parsedOptions = parseScriptOptions(options, restrictSchemes); const script = new WebExtensionContentScript(policy, parsedOptions); policy.registerContentScript(script); registeredContentScripts.set(scriptId, script); } } policy.active = true; policy.initData = extension; @@ -426,17 +437,17 @@ ExtensionManager = { if (policy) { const registeredContentScripts = this.registeredContentScripts.get(policy); if (registeredContentScripts.has(data.scriptId)) { Cu.reportError(new Error( `Registering content script ${data.scriptId} on ${data.id} more than once`)); } else { try { - const parsedOptions = parseScriptOptions(data.options); + const parsedOptions = parseScriptOptions(data.options, !policy.hasPermission("mozillaAddons")); const script = new WebExtensionContentScript(policy, parsedOptions); policy.registerContentScript(script); registeredContentScripts.set(data.scriptId, script); } catch (e) { Cu.reportError(e); } } }
--- a/toolkit/components/extensions/schemas/manifest.json +++ b/toolkit/components/extensions/schemas/manifest.json @@ -448,16 +448,19 @@ "id": "MatchPattern", "choices": [ { "type": "string", "enum": ["<all_urls>"] }, { "$ref": "MatchPatternRestricted" + }, + { + "$ref": "MatchPatternUnestricted" } ] }, { "id": "MatchPatternRestricted", "description": "Same as MatchPattern above, but excludes <all_urls>", "choices": [ { @@ -466,16 +469,26 @@ }, { "type": "string", "pattern": "^file:///.*$" } ] }, { + "id": "MatchPatternUnestricted", + "description": "Mostly unrestricted match patterns for privileged add-ons. This should technically be rejected for unprivileged add-ons, but, reasons. The MatchPattern class will still refuse privileged schemes for those extensions.", + "choices": [ + { + "type": "string", + "pattern": "^resource://(\\*|\\*\\.[^*/]+|[^*/]+)/.*$|^about:" + } + ] + }, + { "id": "MatchPatternInternal", "description": "Same as MatchPattern above, but includes moz-extension protocol", "choices": [ { "type": "string", "enum": ["<all_urls>"] }, {
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_restrictSchemes.js @@ -0,0 +1,54 @@ +"use strict"; + +function makeExtension(id, isPrivileged) { + return ExtensionTestUtils.loadExtension({ + isPrivileged, + + manifest: { + applications: {gecko: {id}}, + + permissions: isPrivileged ? ["mozillaAddons"] : [], + + content_scripts: [ + { + "matches": ["resource://foo/file_sample.html"], + "js": ["content_script.js"], + "run_at": "document_start", + }, + ], + }, + + files: { + "content_script.js"() { + browser.test.assertEq("resource://foo/file_sample.html", document.documentURI, + `Loaded content script into the correct document (extension: ${browser.runtime.id})`); + browser.test.sendMessage(`content-script-${browser.runtime.id}`); + }, + }, + }); +} + +add_task(async function test_contentscript_restrictSchemes() { + let resProto = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); + resProto.setSubstitutionWithFlags("foo", Services.io.newFileURI(do_get_file("data")), + resProto.ALLOW_CONTENT_ACCESS); + + let unprivileged = makeExtension("unprivileged@tests.mozilla.org", false); + let privileged = makeExtension("privileged@tests.mozilla.org", true); + + await unprivileged.startup(); + await privileged.startup(); + + unprivileged.onMessage("content-script-unprivileged@tests.mozilla.org", () => { + ok(false, "Unprivileged extension executed content script on resource URL"); + }); + + let contentPage = await ExtensionTestUtils.loadContentPage(`resource://foo/file_sample.html`); + + await privileged.awaitMessage("content-script-privileged@tests.mozilla.org"); + + await contentPage.close(); + + await privileged.unload(); + await unprivileged.unload(); +});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini @@ -22,16 +22,17 @@ skip-if = os == "android" [test_ext_content_security_policy.js] [test_ext_contentscript_api_injection.js] [test_ext_contentscript_async_loading.js] skip-if = os == 'android' && debug # The generated script takes too long to load on Android debug [test_ext_contentscript_context.js] [test_ext_contentscript_create_iframe.js] [test_ext_contentscript_css.js] [test_ext_contentscript_exporthelpers.js] +[test_ext_contentscript_restrictSchemes.js] [test_ext_contentscript_teardown.js] [test_ext_contextual_identities.js] skip-if = os == "android" # Containers are not exposed to android. [test_ext_debugging_utils.js] [test_ext_dns.js] [test_ext_downloads.js] [test_ext_downloads_download.js] skip-if = os == "android"
--- a/xpcom/ds/nsGkAtomList.h +++ b/xpcom/ds/nsGkAtomList.h @@ -1809,16 +1809,17 @@ GK_ATOM(ondevicelight, "ondevicelight") GK_ATOM(ondevicechange, "ondevicechange") // WebExtensions GK_ATOM(moz_extension, "moz-extension") GK_ATOM(all_urlsPermission, "<all_urls>") GK_ATOM(clipboardRead, "clipboardRead") GK_ATOM(clipboardWrite, "clipboardWrite") GK_ATOM(debugger, "debugger") +GK_ATOM(mozillaAddons, "mozillaAddons") GK_ATOM(tabs, "tabs") GK_ATOM(webRequestBlocking, "webRequestBlocking") GK_ATOM(http, "http") GK_ATOM(https, "https") GK_ATOM(proxy, "proxy") //--------------------------------------------------------------------------- // Special atoms