diff --git a/dom/chrome-webidl/WebExtensionPolicy.webidl b/dom/chrome-webidl/WebExtensionPolicy.webidl index 18df0cb0c5c7471d3052a8c00ca0efa28af06e7e..669e262621f820b4478d895aa1d0f37462b42729 100644 --- a/dom/chrome-webidl/WebExtensionPolicy.webidl +++ b/dom/chrome-webidl/WebExtensionPolicy.webidl @@ -144,6 +144,13 @@ interface WebExtensionPolicy { */ static readonly attribute boolean backgroundServiceWorkerEnabled; + /** + * Whether the Quarantined Domains feature is enabled. Use this as a single + * source of truth instead of checking extensions.QuarantinedDomains.enabled + * pref directly because the logic might change. + */ + static readonly attribute boolean quarantinedDomainsEnabled; + /** * Set based on the manifest.incognito value: * If "spanning" or "split" will be true. @@ -171,6 +178,16 @@ interface WebExtensionPolicy { */ boolean hasPermission(DOMString permission); + /** + * Returns true if the domain is on the Quarantined Domains list. + */ + static boolean isQuarantinedURI(URI uri); + + /** + * Returns true if this extension is quarantined from the URI. + */ + boolean quarantinedFromURI(URI uri); + /** * Returns true if the given path relative to the extension's moz-extension: * URL root is listed as a web accessible path. Access checks on a path, such @@ -297,6 +314,8 @@ dictionary WebExtensionInit { boolean isPrivileged = false; + boolean ignoreQuarantine = false; + boolean temporarilyInstalled = false; required WebExtensionLocalizeCallback localizeCallback; diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 92698014cd7d9afe788b1d41be478a4d29b3ea27..e3ac0c812dd6f876211cfda44e15d5173aaeee42 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -3300,6 +3300,10 @@ pref("extensions.webextensions.keepUuidOnUninstall", false); pref("extensions.webextensions.identity.redirectDomain", "extensions.allizom.org"); pref("extensions.webextensions.restrictedDomains", "accounts-static.cdn.mozilla.net,accounts.firefox.com,addons.cdn.mozilla.net,addons.mozilla.org,api.accounts.firefox.com,content.cdn.mozilla.net,discovery.addons.mozilla.org,install.mozilla.org,oauth.accounts.firefox.com,profile.accounts.firefox.com,support.mozilla.org,sync.services.mozilla.com"); +// Extensions are prevented from accessing Quarantined Domains by default. +pref("extensions.quarantinedDomains.enabled", true); +pref("extensions.quarantinedDomains.list", ""); + // Whether or not the moz-extension resource loads are remoted. For debugging // purposes only. Setting this to false will break moz-extension URI loading // unless other process sandboxing and extension remoting prefs are changed. diff --git a/toolkit/components/extensions/ExtensionPolicyService.cpp b/toolkit/components/extensions/ExtensionPolicyService.cpp index bcf63d1e8c950c28343b7ce865cb072576e93659..833eedffa3b511db70a250fa91027dc2ba58976e 100644 --- a/toolkit/components/extensions/ExtensionPolicyService.cpp +++ b/toolkit/components/extensions/ExtensionPolicyService.cpp @@ -56,6 +56,9 @@ using dom::Promise; #define RESTRICTED_DOMAINS_PREF "extensions.webextensions.restrictedDomains" +#define QUARANTINED_DOMAINS_PREF "extensions.quarantinedDomains.list" +#define QUARANTINED_DOMAINS_ENABLED "extensions.quarantinedDomains.enabled" + #define OBS_TOPIC_PRELOAD_SCRIPT "web-extension-preload-content-script" #define OBS_TOPIC_LOAD_SCRIPT "web-extension-load-content-script" @@ -71,6 +74,7 @@ using CoreByHostMap = nsTHashMap<nsCStringASCIICaseInsensitiveHashKey, static StaticRWLock sEPSLock; static StaticAutoPtr<CoreByHostMap> sCoreByHost MOZ_GUARDED_BY(sEPSLock); static StaticRefPtr<AtomSet> sRestrictedDomains MOZ_GUARDED_BY(sEPSLock); +static StaticRefPtr<AtomSet> sQuarantinedDomains MOZ_GUARDED_BY(sEPSLock); /* static */ mozIExtensionProcessScript& ExtensionPolicyService::ProcessScript() { @@ -124,6 +128,7 @@ ExtensionPolicyService::ExtensionPolicyService() { } UpdateRestrictedDomains(); + UpdateQuarantinedDomains(); } ExtensionPolicyService::~ExtensionPolicyService() { @@ -133,6 +138,7 @@ ExtensionPolicyService::~ExtensionPolicyService() { StaticAutoWriteLock lock(sEPSLock); sCoreByHost = nullptr; sRestrictedDomains = nullptr; + sQuarantinedDomains = nullptr; } } @@ -154,6 +160,10 @@ bool ExtensionPolicyService::IsExtensionProcess() const { return !isRemote && XRE_IsParentProcess(); } +bool ExtensionPolicyService::GetQuarantinedDomainsEnabled() const { + return Preferences::GetBool(QUARANTINED_DOMAINS_ENABLED); +} + WebExtensionPolicy* ExtensionPolicyService::GetByURL(const URLInfo& aURL) { if (aURL.Scheme() == nsGkAtoms::moz_extension) { return GetByHost(aURL.Host()); @@ -268,6 +278,8 @@ void ExtensionPolicyService::RegisterObservers() { Preferences::AddStrongObserver(this, DEFAULT_CSP_PREF); Preferences::AddStrongObserver(this, DEFAULT_CSP_PREF_V3); Preferences::AddStrongObserver(this, RESTRICTED_DOMAINS_PREF); + Preferences::AddStrongObserver(this, QUARANTINED_DOMAINS_PREF); + Preferences::AddStrongObserver(this, QUARANTINED_DOMAINS_ENABLED); } void ExtensionPolicyService::UnregisterObservers() { @@ -280,6 +292,8 @@ void ExtensionPolicyService::UnregisterObservers() { Preferences::RemoveObserver(this, DEFAULT_CSP_PREF); Preferences::RemoveObserver(this, DEFAULT_CSP_PREF_V3); Preferences::RemoveObserver(this, RESTRICTED_DOMAINS_PREF); + Preferences::RemoveObserver(this, QUARANTINED_DOMAINS_PREF); + Preferences::RemoveObserver(this, QUARANTINED_DOMAINS_ENABLED); } nsresult ExtensionPolicyService::Observe(nsISupports* aSubject, @@ -305,6 +319,9 @@ nsresult ExtensionPolicyService::Observe(nsISupports* aSubject, mDefaultCSPV3.SetIsVoid(true); } else if (!strcmp(pref, RESTRICTED_DOMAINS_PREF)) { UpdateRestrictedDomains(); + } else if (!strcmp(pref, QUARANTINED_DOMAINS_PREF) || + !strcmp(pref, QUARANTINED_DOMAINS_ENABLED)) { + UpdateQuarantinedDomains(); } } return NS_OK; @@ -571,6 +588,12 @@ RefPtr<AtomSet> ExtensionPolicyService::RestrictedDomains() { return sRestrictedDomains; } +/* static */ +RefPtr<AtomSet> ExtensionPolicyService::QuarantinedDomains() { + StaticAutoReadLock lock(sEPSLock); + return sQuarantinedDomains; +} + void ExtensionPolicyService::UpdateRestrictedDomains() { nsAutoCString eltsString; Unused << Preferences::GetCString(RESTRICTED_DOMAINS_PREF, eltsString); @@ -586,6 +609,28 @@ void ExtensionPolicyService::UpdateRestrictedDomains() { sRestrictedDomains = atomSet; } +void ExtensionPolicyService::UpdateQuarantinedDomains() { + if (!GetQuarantinedDomainsEnabled()) { + StaticAutoWriteLock lock(sEPSLock); + sQuarantinedDomains = nullptr; + return; + } + + nsAutoCString eltsString; + AutoTArray<nsString, 32> elts; + if (NS_SUCCEEDED( + Preferences::GetCString(QUARANTINED_DOMAINS_PREF, eltsString))) { + for (const nsACString& elt : eltsString.Split(',')) { + elts.AppendElement(NS_ConvertUTF8toUTF16(elt)); + elts.LastElement().StripWhitespace(); + } + } + RefPtr<AtomSet> atomSet = new AtomSet(elts); + + StaticAutoWriteLock lock(sEPSLock); + sQuarantinedDomains = atomSet; +} + /***************************************************************************** * nsIAddonPolicyService *****************************************************************************/ diff --git a/toolkit/components/extensions/ExtensionPolicyService.h b/toolkit/components/extensions/ExtensionPolicyService.h index fe73c5b19c3c158039a2d8e32fd24ff4b22ae8e9..b43b52080b2c61026532ccda92745dc73fd5c9a4 100644 --- a/toolkit/components/extensions/ExtensionPolicyService.h +++ b/toolkit/components/extensions/ExtensionPolicyService.h @@ -56,11 +56,14 @@ class ExtensionPolicyService final : public nsIAddonPolicyService, static ExtensionPolicyService& GetSingleton(); - // Heper for fetching an AtomSet of restricted domains as configured by the + // Helper for fetching an AtomSet of restricted domains as configured by the // extensions.webextensions.restrictedDomains pref. Safe to call from any // thread. static RefPtr<extensions::AtomSet> RestrictedDomains(); + // Thread-safe AtomSet from extensions.quarantinedDomains.list. + static RefPtr<extensions::AtomSet> QuarantinedDomains(); + static already_AddRefed<ExtensionPolicyService> GetInstance() { return do_AddRef(&GetSingleton()); } @@ -94,6 +97,7 @@ class ExtensionPolicyService final : public nsIAddonPolicyService, bool UseRemoteExtensions() const; bool IsExtensionProcess() const; + bool GetQuarantinedDomainsEnabled() const; nsresult InjectContentScripts(WebExtensionPolicy* aExtension); @@ -120,6 +124,7 @@ class ExtensionPolicyService final : public nsIAddonPolicyService, const nsTArray<RefPtr<extensions::WebExtensionContentScript>>& aScripts); void UpdateRestrictedDomains(); + void UpdateQuarantinedDomains(); nsRefPtrHashtable<nsPtrHashKey<const nsAtom>, WebExtensionPolicy> mExtensions; diff --git a/toolkit/components/extensions/WebExtensionPolicy.cpp b/toolkit/components/extensions/WebExtensionPolicy.cpp index 03467a722f9d8d113d08ff1f848592beba11522e..7613a11a4f3e218fa565278b9a6b0771894436c8 100644 --- a/toolkit/components/extensions/WebExtensionPolicy.cpp +++ b/toolkit/components/extensions/WebExtensionPolicy.cpp @@ -187,6 +187,7 @@ WebExtensionPolicyCore::WebExtensionPolicyCore(GlobalObject& aGlobal, mManifestVersion(aInit.mManifestVersion), mExtensionPageCSP(aInit.mExtensionPageCSP), mIsPrivileged(aInit.mIsPrivileged), + mIgnoreQuarantine(aInit.mIsPrivileged || aInit.mIgnoreQuarantine), mTemporarilyInstalled(aInit.mTemporarilyInstalled), mBackgroundWorkerScript(aInit.mBackgroundWorkerScript), mPermissions(new AtomSet(aInit.mPermissions)) { @@ -262,6 +263,9 @@ bool WebExtensionPolicyCore::CanAccessURI(const URLInfo& aURI, bool aExplicit, if (aCheckRestricted && WebExtensionPolicy::IsRestrictedURI(aURI)) { return false; } + if (aCheckRestricted && QuarantinedFromURI(aURI)) { + return false; + } if (!aAllowFilePermission && aURI.Scheme() == nsGkAtoms::file) { return false; } @@ -270,6 +274,14 @@ bool WebExtensionPolicyCore::CanAccessURI(const URLInfo& aURI, bool aExplicit, return mHostPermissions && mHostPermissions->Matches(aURI, aExplicit); } +bool WebExtensionPolicyCore::QuarantinedFromDoc(const DocInfo& aDoc) const { + return QuarantinedFromURI(aDoc.PrincipalURL()); +} + +bool WebExtensionPolicyCore::QuarantinedFromURI(const URLInfo& aURI) const { + return !mIgnoreQuarantine && WebExtensionPolicy::IsQuarantinedURI(aURI); +} + /***************************************************************************** * WebExtensionPolicy *****************************************************************************/ @@ -493,6 +505,11 @@ bool WebExtensionPolicy::BackgroundServiceWorkerEnabled(GlobalObject& aGlobal) { return StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup(); } +/* static */ +bool WebExtensionPolicy::QuarantinedDomainsEnabled(GlobalObject& aGlobal) { + return EPS().GetQuarantinedDomainsEnabled(); +} + /* static */ bool WebExtensionPolicy::IsRestrictedDoc(const DocInfo& aDoc) { // With the exception of top-level about:blank documents with null @@ -521,6 +538,22 @@ bool WebExtensionPolicy::IsRestrictedURI(const URLInfo& aURI) { return false; } +/* static */ +bool WebExtensionPolicy::IsQuarantinedDoc(const DocInfo& aDoc) { + return IsQuarantinedURI(aDoc.PrincipalURL()); +} + +/* static */ +bool WebExtensionPolicy::IsQuarantinedURI(const URLInfo& aURI) { + // Ensure EPS is initialized before asking it about quarantined domains. + Unused << EPS(); + + RefPtr<AtomSet> quarantinedDomains = + ExtensionPolicyService::QuarantinedDomains(); + + return quarantinedDomains && quarantinedDomains->Contains(aURI.HostAtom()); +} + nsCString WebExtensionPolicy::BackgroundPageHTML() const { nsCString result; @@ -780,7 +813,11 @@ bool MozDocumentMatcher::Matches(const DocInfo& aDoc, return true; } - if (mRestricted && mExtension && mExtension->IsRestrictedDoc(aDoc)) { + if (mRestricted && WebExtensionPolicy::IsRestrictedDoc(aDoc)) { + return false; + } + + if (mRestricted && mExtension && mExtension->QuarantinedFromDoc(aDoc)) { return false; } @@ -821,7 +858,11 @@ bool MozDocumentMatcher::MatchesURI(const URLInfo& aURL, return false; } - if (mRestricted && mExtension->IsRestrictedURI(aURL)) { + if (mRestricted && WebExtensionPolicy::IsRestrictedURI(aURL)) { + return false; + } + + if (mRestricted && mExtension->QuarantinedFromURI(aURL)) { return false; } diff --git a/toolkit/components/extensions/WebExtensionPolicy.h b/toolkit/components/extensions/WebExtensionPolicy.h index ef0f35f886e38b9668c56b2852b0497f41eeb17b..23b71673c48bef970b9cc642072fd77bf6ead903 100644 --- a/toolkit/components/extensions/WebExtensionPolicy.h +++ b/toolkit/components/extensions/WebExtensionPolicy.h @@ -133,6 +133,9 @@ class WebExtensionPolicyCore final { bool aCheckRestricted = true, bool aAllowFilePermission = false) const; + bool QuarantinedFromDoc(const DocInfo& aDoc) const; + bool QuarantinedFromURI(const URLInfo& aURI) const; + // Try to get a reference to the cycle-collected main-thread-only // WebExtensionPolicy instance. // @@ -172,6 +175,7 @@ class WebExtensionPolicyCore final { /* const */ nsString mBaseCSP; const bool mIsPrivileged; + const bool mIgnoreQuarantine; const bool mTemporarilyInstalled; const nsString mBackgroundWorkerScript; @@ -253,6 +257,17 @@ class WebExtensionPolicy final : public nsISupports, public nsWrapperCache { static bool IsRestrictedDoc(const DocInfo& aDoc); static bool IsRestrictedURI(const URLInfo& aURI); + static bool IsQuarantinedDoc(const DocInfo& aDoc); + static bool IsQuarantinedURI(const URLInfo& aURI); + + bool QuarantinedFromDoc(const DocInfo& aDoc) const { + return mCore->QuarantinedFromDoc(aDoc); + } + + bool QuarantinedFromURI(const URLInfo& aURI) const { + return mCore->QuarantinedFromURI(aURI); + } + nsCString BackgroundPageHTML() const; MOZ_CAN_RUN_SCRIPT @@ -333,9 +348,20 @@ class WebExtensionPolicy final : public nsISupports, public nsWrapperCache { return IsRestrictedURI(aURI); } + static bool IsQuarantinedURI(dom::GlobalObject& aGlobal, + const URLInfo& aURI) { + return IsQuarantinedURI(aURI); + } + + bool QuarantinedFromURI(dom::GlobalObject& aGlobal, + const URLInfo& aURI) const { + return QuarantinedFromURI(aURI); + } + static bool UseRemoteWebExtensions(dom::GlobalObject& aGlobal); static bool IsExtensionProcess(dom::GlobalObject& aGlobal); static bool BackgroundServiceWorkerEnabled(dom::GlobalObject& aGlobal); + static bool QuarantinedDomainsEnabled(dom::GlobalObject& aGlobal); nsISupports* GetParentObject() const { return mParent; } diff --git a/toolkit/components/extensions/test/xpcshell/test_QuarantinedDomains.js b/toolkit/components/extensions/test/xpcshell/test_QuarantinedDomains.js new file mode 100644 index 0000000000000000000000000000000000000000..e86da2f96b91925b92d437837bc0f74c3e68e418 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_QuarantinedDomains.js @@ -0,0 +1,161 @@ +"use strict"; + +const DOMAINS = [ + "addons-dev.allizom.org", + "mixed.badssl.com", + "careers.mozilla.com", + "developer.mozilla.org", + "test.example.com", +]; + +function makePolicy(options) { + return new WebExtensionPolicy({ + baseURL: "file:///foo/", + localizeCallback: str => str, + allowedOrigins: new MatchPatternSet(["<all_urls>"], { ignorePath: true }), + mozExtensionHostname: Services.uuid.generateUUID().toString().slice(1, -1), + ...options, + }); +} + +function makeCS(policy) { + return new WebExtensionContentScript(policy, { + matches: new MatchPatternSet(["<all_urls>"]), + }); +} + +function expectQuarantined(expectedDomains) { + for (let domain of DOMAINS) { + let uri = Services.io.newURI(`https://${domain}/`); + let quarantined = expectedDomains.includes(domain); + + equal( + quarantined, + WebExtensionPolicy.isQuarantinedURI(uri), + `Expect ${uri.spec} to ${quarantined ? "" : "not"} be quarantined.` + ); + } +} + +function expectAccess(policy, cs, expected) { + for (let domain of DOMAINS) { + let uri = Services.io.newURI(`https://${domain}/`); + let access = expected[domain]; + let match = access; + + equal( + access, + !policy.quarantinedFromURI(uri), + `${policy.id} is ${access ? "not" : ""} quarantined from ${uri.spec}.` + ); + equal( + access, + policy.canAccessURI(uri), + `Expect ${policy.id} ${access ? "can" : "can't"} access ${uri.spec}.` + ); + + equal( + match, + cs.matchesURI(uri), + `Expect ${cs.extension.id} to ${match ? "" : "not"} match ${uri.spec}.` + ); + } +} + +function expectHost(desc, host, quarantined) { + let uri = Services.io.newURI(`https://${host}/`); + equal( + quarantined, + WebExtensionPolicy.isQuarantinedURI(uri), + `Expect ${desc} "${host}" to ${quarantined ? "" : "not"} be quarantined.` + ); +} + +add_task(async function test_QuarantinedDomains() { + let plain = makePolicy({ id: "plain@test" }); + let system = makePolicy({ id: "system@test", isPrivileged: true }); + let exempt = makePolicy({ id: "exempt@test", ignoreQuarantine: true }); + + let plainCS = makeCS(plain); + let systemCS = makeCS(system); + let exemptCS = makeCS(exempt); + + const canAccessAll = { + "addons-dev.allizom.org": true, + "mixed.badssl.com": true, + "careers.mozilla.com": true, + "developer.mozilla.org": true, + "test.example.com": true, + }; + + info("Initial pref state is an empty list."); + expectQuarantined([]); + + expectAccess(plain, plainCS, canAccessAll); + expectAccess(system, systemCS, canAccessAll); + expectAccess(exempt, exemptCS, canAccessAll); + + info("Default test domain list."); + Services.prefs.setStringPref( + "extensions.quarantinedDomains.list", + "addons-dev.allizom.org,mixed.badssl.com,test.example.com" + ); + + expectQuarantined([ + "addons-dev.allizom.org", + "mixed.badssl.com", + "test.example.com", + ]); + + expectAccess(plain, plainCS, { + "addons-dev.allizom.org": false, + "mixed.badssl.com": false, + "careers.mozilla.com": true, + "developer.mozilla.org": true, + "test.example.com": false, + }); + + expectAccess(system, systemCS, canAccessAll); + expectAccess(exempt, exemptCS, canAccessAll); + + info("Disable the Quarantined Domains feature."); + Services.prefs.setBoolPref("extensions.quarantinedDomains.enabled", false); + expectQuarantined([]); + + expectAccess(plain, plainCS, canAccessAll); + expectAccess(system, systemCS, canAccessAll); + expectAccess(exempt, exemptCS, canAccessAll); + + info( + "Enable again, drop addons-dev.allizom.org and add developer.mozilla.org to the pref." + ); + Services.prefs.setBoolPref("extensions.quarantinedDomains.enabled", true); + + Services.prefs.setStringPref( + "extensions.quarantinedDomains.list", + "mixed.badssl.com,developer.mozilla.org,test.example.com" + ); + expectQuarantined([ + "mixed.badssl.com", + "developer.mozilla.org", + "test.example.com", + ]); + + expectAccess(plain, plainCS, { + "addons-dev.allizom.org": true, + "mixed.badssl.com": false, + "careers.mozilla.com": true, + "developer.mozilla.org": false, + "test.example.com": false, + }); + + expectAccess(system, systemCS, canAccessAll); + expectAccess(exempt, exemptCS, canAccessAll); + + expectHost("host with a port", "test.example.com:1025", true); + + expectHost("FQDN", "test.example.com.", false); + expectHost("subdomain", "subdomain.test.example.com", false); + expectHost("domain with prefix", "pretest.example.com", false); + expectHost("domain with suffix", "test.example.comsuf", false); +}); diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini index 7ecd78f795a5b45f31dd1d7fa12966d0b98afce3..f76456124d7fd5522a02cb57ba39e5f09f25f703 100644 --- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini @@ -422,6 +422,7 @@ skip-if = os == "android" # incognito not supported on android [test_proxy_info_results.js] skip-if = os == "win" # bug 1802704 [test_proxy_userContextId.js] +[test_QuarantinedDomains.js] [test_site_permissions.js] [test_webRequest_ancestors.js] [test_webRequest_cookies.js]