/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/ExtensionPolicyService.h" #include "mozilla/extensions/DocumentObserver.h" #include "mozilla/extensions/WebExtensionContentScript.h" #include "mozilla/extensions/WebExtensionPolicy.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Preferences.h" #include "mozilla/ResultExtensions.h" #include "mozilla/Services.h" #include "mozilla/SimpleEnumerator.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentFrameMessageManager.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/Promise-inl.h" #include "mozIExtensionProcessScript.h" #include "nsEscape.h" #include "nsGkAtoms.h" #include "nsIChannel.h" #include "nsIContentPolicy.h" #include "nsIDocShell.h" #include "nsIDocument.h" #include "nsGlobalWindowOuter.h" #include "nsILoadInfo.h" #include "nsIXULRuntime.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nsPIDOMWindow.h" #include "nsXULAppAPI.h" #include "nsQueryObject.h" namespace mozilla { using namespace extensions; using dom::AutoJSAPI; using dom::ContentFrameMessageManager; using dom::Promise; #define DEFAULT_BASE_CSP \ "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " \ "object-src 'self' https://* moz-extension: blob: filesystem:;" #define DEFAULT_DEFAULT_CSP \ "script-src 'self'; object-src 'self';" #define OBS_TOPIC_PRELOAD_SCRIPT "web-extension-preload-content-script" #define OBS_TOPIC_LOAD_SCRIPT "web-extension-load-content-script" static mozIExtensionProcessScript& ProcessScript() { static nsCOMPtr sProcessScript; if (MOZ_UNLIKELY(!sProcessScript)) { sProcessScript = do_GetService("@mozilla.org/webextensions/extension-process-script;1"); MOZ_RELEASE_ASSERT(sProcessScript); ClearOnShutdown(&sProcessScript); } return *sProcessScript; } /***************************************************************************** * ExtensionPolicyService *****************************************************************************/ /* static */ bool ExtensionPolicyService::sRemoteExtensions; /* static */ ExtensionPolicyService& ExtensionPolicyService::GetSingleton() { static RefPtr sExtensionPolicyService; if (MOZ_UNLIKELY(!sExtensionPolicyService)) { sExtensionPolicyService = new ExtensionPolicyService(); RegisterWeakMemoryReporter(sExtensionPolicyService); ClearOnShutdown(&sExtensionPolicyService); } return *sExtensionPolicyService.get(); } ExtensionPolicyService::ExtensionPolicyService() { mObs = services::GetObserverService(); MOZ_RELEASE_ASSERT(mObs); Preferences::AddBoolVarCache(&sRemoteExtensions, "extensions.webextensions.remote", false); RegisterObservers(); } ExtensionPolicyService::~ExtensionPolicyService() { UnregisterWeakMemoryReporter(this); } bool ExtensionPolicyService::UseRemoteExtensions() const { return sRemoteExtensions && BrowserTabsRemoteAutostart(); } bool ExtensionPolicyService::IsExtensionProcess() const { bool isRemote = UseRemoteExtensions(); if (isRemote && XRE_IsContentProcess()) { auto& remoteType = dom::ContentChild::GetSingleton()->GetRemoteType(); return remoteType.EqualsLiteral(EXTENSION_REMOTE_TYPE); } return !isRemote && XRE_IsParentProcess(); } WebExtensionPolicy* ExtensionPolicyService::GetByURL(const URLInfo& aURL) { if (aURL.Scheme() == nsGkAtoms::moz_extension) { return GetByHost(aURL.Host()); } return nullptr; } void ExtensionPolicyService::GetAll(nsTArray>& aResult) { for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) { aResult.AppendElement(iter.Data()); } } bool ExtensionPolicyService::RegisterExtension(WebExtensionPolicy& aPolicy) { bool ok = (!GetByID(aPolicy.Id()) && !GetByHost(aPolicy.MozExtensionHostname())); MOZ_ASSERT(ok); if (!ok) { return false; } mExtensions.Put(aPolicy.Id(), &aPolicy); mExtensionHosts.Put(aPolicy.MozExtensionHostname(), &aPolicy); return true; } bool ExtensionPolicyService::UnregisterExtension(WebExtensionPolicy& aPolicy) { bool ok = (GetByID(aPolicy.Id()) == &aPolicy && GetByHost(aPolicy.MozExtensionHostname()) == &aPolicy); MOZ_ASSERT(ok); if (!ok) { return false; } mExtensions.Remove(aPolicy.Id()); mExtensionHosts.Remove(aPolicy.MozExtensionHostname()); return true; } bool ExtensionPolicyService::RegisterObserver(DocumentObserver& aObserver) { if (mObservers.GetWeak(&aObserver)) { return false; } mObservers.Put(&aObserver, &aObserver); return true; } bool ExtensionPolicyService::UnregisterObserver(DocumentObserver& aObserver) { if (!mObservers.GetWeak(&aObserver)) { return false; } mObservers.Remove(&aObserver); return true; } void ExtensionPolicyService::BaseCSP(nsAString& aBaseCSP) const { nsresult rv; rv = Preferences::GetString("extensions.webextensions.base-content-security-policy", aBaseCSP); if (NS_FAILED(rv)) { aBaseCSP.AssignLiteral(DEFAULT_BASE_CSP); } } void ExtensionPolicyService::DefaultCSP(nsAString& aDefaultCSP) const { nsresult rv; rv = Preferences::GetString("extensions.webextensions.default-content-security-policy", aDefaultCSP); if (NS_FAILED(rv)) { aDefaultCSP.AssignLiteral(DEFAULT_DEFAULT_CSP); } } /***************************************************************************** * nsIMemoryReporter *****************************************************************************/ NS_IMETHODIMP ExtensionPolicyService::CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, bool aAnonymize) { for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) { auto& ext = iter.Data(); nsAtomCString id(ext->Id()); NS_ConvertUTF16toUTF8 name(ext->Name()); name.ReplaceSubstring("\"", ""); name.ReplaceSubstring("\\", ""); nsString url; MOZ_TRY_VAR(url, ext->GetURL(NS_LITERAL_STRING(""))); nsPrintfCString desc("Extension(id=%s, name=\"%s\", baseURL=%s)", id.get(), name.get(), NS_ConvertUTF16toUTF8(url).get()); desc.ReplaceChar('/', '\\'); nsCString path("extensions/"); path.Append(desc); aHandleReport->Callback( EmptyCString(), path, KIND_NONHEAP, UNITS_COUNT, 1, NS_LITERAL_CSTRING("WebExtensions that are active in this session"), aData); } return NS_OK; } /***************************************************************************** * Content script management *****************************************************************************/ void ExtensionPolicyService::RegisterObservers() { mObs->AddObserver(this, "content-document-global-created", false); mObs->AddObserver(this, "document-element-inserted", false); mObs->AddObserver(this, "tab-content-frameloader-created", false); if (XRE_IsContentProcess()) { mObs->AddObserver(this, "http-on-opening-request", false); } } void ExtensionPolicyService::UnregisterObservers() { mObs->RemoveObserver(this, "content-document-global-created"); mObs->RemoveObserver(this, "document-element-inserted"); mObs->RemoveObserver(this, "tab-content-frameloader-created"); if (XRE_IsContentProcess()) { mObs->RemoveObserver(this, "http-on-opening-request"); } } nsresult ExtensionPolicyService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, "content-document-global-created")) { nsCOMPtr win = do_QueryInterface(aSubject); if (win) { CheckWindow(win); } } else if (!strcmp(aTopic, "document-element-inserted")) { nsCOMPtr doc = do_QueryInterface(aSubject); if (doc) { CheckDocument(doc); } } else if (!strcmp(aTopic, "http-on-opening-request")) { nsCOMPtr chan = do_QueryInterface(aSubject); if (chan) { CheckRequest(chan); } } else if (!strcmp(aTopic, "tab-content-frameloader-created")) { RefPtr mm = do_QueryObject(aSubject); NS_ENSURE_TRUE(mm, NS_ERROR_UNEXPECTED); mMessageManagers.PutEntry(mm); mm->AddSystemEventListener(NS_LITERAL_STRING("unload"), this, false, false); } return NS_OK; } nsresult ExtensionPolicyService::HandleEvent(dom::Event* aEvent) { RefPtr mm = do_QueryObject(aEvent->GetTarget()); MOZ_ASSERT(mm); if (mm) { mMessageManagers.RemoveEntry(mm); } return NS_OK; } nsresult ForEachDocShell(nsIDocShell* aDocShell, const std::function& aCallback) { nsCOMPtr iter; MOZ_TRY(aDocShell->GetDocShellEnumerator(nsIDocShell::typeContent, nsIDocShell::ENUMERATE_FORWARDS, getter_AddRefs(iter))); for (auto& docShell : SimpleEnumerator(iter)) { MOZ_TRY(aCallback(docShell)); } return NS_OK; } already_AddRefed ExtensionPolicyService::ExecuteContentScript(nsPIDOMWindowInner* aWindow, WebExtensionContentScript& aScript) { if (!aWindow->IsCurrentInnerWindow()) { return nullptr; } RefPtr promise; ProcessScript().LoadContentScript(&aScript, aWindow, getter_AddRefs(promise)); return promise.forget(); } RefPtr ExtensionPolicyService::ExecuteContentScripts(JSContext* aCx, nsPIDOMWindowInner* aWindow, const nsTArray>& aScripts) { AutoTArray, 8> promises; for (auto& script : aScripts) { if (RefPtr promise = ExecuteContentScript(aWindow, *script)) { promises.AppendElement(std::move(promise)); } } RefPtr promise = Promise::All(aCx, promises, IgnoreErrors()); MOZ_RELEASE_ASSERT(promise); return promise; } nsresult ExtensionPolicyService::InjectContentScripts(WebExtensionPolicy* aExtension) { AutoJSAPI jsapi; MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); for (auto iter = mMessageManagers.ConstIter(); !iter.Done(); iter.Next()) { ContentFrameMessageManager* mm = iter.Get()->GetKey(); nsCOMPtr docShell = mm->GetDocShell(IgnoreErrors()); NS_ENSURE_TRUE(docShell, NS_ERROR_UNEXPECTED); auto result = ForEachDocShell(docShell, [&](nsIDocShell* aDocShell) -> nsresult { nsCOMPtr win = aDocShell->GetWindow(); if (!win->GetDocumentURI()) { return NS_OK; } DocInfo docInfo(win); using RunAt = dom::ContentScriptRunAt; using Scripts = AutoTArray, 8>; constexpr uint8_t n = uint8_t(RunAt::EndGuard_); Scripts scripts[n]; auto GetScripts = [&](RunAt aRunAt) -> Scripts&& { return std::move(scripts[uint8_t(aRunAt)]); }; for (const auto& script : aExtension->ContentScripts()) { if (script->Matches(docInfo)) { GetScripts(script->RunAt()).AppendElement(script); } } nsCOMPtr inner = win->GetCurrentInnerWindow(); MOZ_TRY(ExecuteContentScripts(jsapi.cx(), inner, GetScripts(RunAt::Document_start)) ->ThenWithCycleCollectedArgs([](JSContext* aCx, JS::HandleValue aValue, ExtensionPolicyService* aSelf, nsPIDOMWindowInner* aInner, Scripts&& aScripts) { return aSelf->ExecuteContentScripts(aCx, aInner, aScripts).forget(); }, this, inner, GetScripts(RunAt::Document_end)) .andThen([&](auto aPromise) { return aPromise->ThenWithCycleCollectedArgs([](JSContext* aCx, JS::HandleValue aValue, ExtensionPolicyService* aSelf, nsPIDOMWindowInner* aInner, Scripts&& aScripts) { return aSelf->ExecuteContentScripts(aCx, aInner, aScripts).forget(); }, this, inner, GetScripts(RunAt::Document_idle)); })); return NS_OK; }); MOZ_TRY(result); } return NS_OK; } // Checks a request for matching content scripts, and begins pre-loading them // if necessary. void ExtensionPolicyService::CheckRequest(nsIChannel* aChannel) { nsCOMPtr loadInfo = aChannel->GetLoadInfo(); if (!loadInfo) { return; } auto loadType = loadInfo->GetExternalContentPolicyType(); if (loadType != nsIContentPolicy::TYPE_DOCUMENT && loadType != nsIContentPolicy::TYPE_SUBDOCUMENT) { return; } nsCOMPtr uri; if (NS_FAILED(aChannel->GetURI(getter_AddRefs(uri)))) { return; } CheckContentScripts({uri.get(), loadInfo}, true); } static bool CheckParentFrames(nsPIDOMWindowOuter* aWindow, WebExtensionPolicy& aPolicy) { nsCOMPtr aboutAddons; if (NS_FAILED(NS_NewURI(getter_AddRefs(aboutAddons), "about:addons"))) { return false; } auto* piWin = aWindow; while ((piWin = piWin->GetScriptableParentOrNull())) { auto* win = nsGlobalWindowOuter::Cast(piWin); auto* principal = BasePrincipal::Cast(win->GetPrincipal()); if (nsContentUtils::IsSystemPrincipal(principal)) { // The add-on manager is a special case, since it contains extension // options pages in same-type frames. bool equals; if (NS_SUCCEEDED(win->GetDocumentURI()->Equals(aboutAddons, &equals)) && equals) { return true; } } if (principal->AddonPolicy() != &aPolicy) { return false; } } return true; } // Checks a document, just after the document element has been inserted, for // matching content scripts or extension principals, and loads them if // necessary. void ExtensionPolicyService::CheckDocument(nsIDocument* aDocument) { nsCOMPtr win = aDocument->GetWindow(); if (win) { nsIDocShell* docShell = win->GetDocShell(); RefPtr mm = docShell->GetMessageManager(); if (!mm || !mMessageManagers.Contains(mm)) { return; } if (win->GetDocumentURI()) { CheckContentScripts(win.get(), false); } nsIPrincipal* principal = aDocument->NodePrincipal(); RefPtr policy = BasePrincipal::Cast(principal)->AddonPolicy(); if (policy) { bool privileged = IsExtensionProcess() && CheckParentFrames(win, *policy); ProcessScript().InitExtensionDocument(policy, aDocument, privileged); } } } // Checks for loads of about:blank into new window globals, and loads any // matching content scripts. about:blank loads do not trigger document element // inserted events, so they're the only load type that are special cased this // way. void ExtensionPolicyService::CheckWindow(nsPIDOMWindowOuter* aWindow) { // We only care about non-initial document loads here. The initial // about:blank document will usually be re-used to load another document. nsCOMPtr doc = aWindow->GetExtantDoc(); if (!doc || doc->IsInitialDocument() || doc->GetReadyStateEnum() == nsIDocument::READYSTATE_UNINITIALIZED) { return; } nsCOMPtr docUri = doc->GetDocumentURI(); nsCOMPtr uri; if (!docUri || NS_FAILED(NS_GetURIWithoutRef(docUri, getter_AddRefs(uri))) || !NS_IsAboutBlank(uri)) { return; } nsIDocShell* docShell = aWindow->GetDocShell(); if (RefPtr mm = docShell->GetMessageManager()) { if (mMessageManagers.Contains(mm)) { CheckContentScripts(aWindow, false); } } } void ExtensionPolicyService::CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload) { nsCOMPtr win; if (!aIsPreload) { win = aDocInfo.GetWindow()->GetCurrentInnerWindow(); } for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) { RefPtr policy = iter.Data(); for (auto& script : policy->ContentScripts()) { if (script->Matches(aDocInfo)) { if (aIsPreload) { ProcessScript().PreloadContentScript(script); } else { if (!win->IsCurrentInnerWindow()) { break; } RefPtr promise; ProcessScript().LoadContentScript(script, win, getter_AddRefs(promise)); } } } } for (auto iter = mObservers.Iter(); !iter.Done(); iter.Next()) { RefPtr observer = iter.Data(); for (auto& matcher : observer->Matchers()) { if (matcher->Matches(aDocInfo)) { if (aIsPreload) { observer->NotifyMatch(*matcher, aDocInfo.GetLoadInfo()); } else { observer->NotifyMatch(*matcher, aDocInfo.GetWindow()); } } } } } /***************************************************************************** * nsIAddonPolicyService *****************************************************************************/ nsresult ExtensionPolicyService::GetBaseCSP(nsAString& aBaseCSP) { BaseCSP(aBaseCSP); return NS_OK; } nsresult ExtensionPolicyService::GetDefaultCSP(nsAString& aDefaultCSP) { DefaultCSP(aDefaultCSP); return NS_OK; } nsresult ExtensionPolicyService::GetAddonCSP(const nsAString& aAddonId, nsAString& aResult) { if (WebExtensionPolicy* policy = GetByID(aAddonId)) { policy->GetContentSecurityPolicy(aResult); return NS_OK; } return NS_ERROR_INVALID_ARG; } nsresult ExtensionPolicyService::GetGeneratedBackgroundPageUrl(const nsACString& aHostname, nsACString& aResult) { if (WebExtensionPolicy* policy = GetByHost(aHostname)) { nsAutoCString url("data:text/html,"); nsCString html = policy->BackgroundPageHTML(); nsAutoCString escaped; url.Append(NS_EscapeURL(html, esc_Minimal, escaped)); aResult = url; return NS_OK; } return NS_ERROR_INVALID_ARG; } nsresult ExtensionPolicyService::AddonHasPermission(const nsAString& aAddonId, const nsAString& aPerm, bool* aResult) { if (WebExtensionPolicy* policy = GetByID(aAddonId)) { *aResult = policy->HasPermission(aPerm); return NS_OK; } return NS_ERROR_INVALID_ARG; } nsresult ExtensionPolicyService::AddonMayLoadURI(const nsAString& aAddonId, nsIURI* aURI, bool aExplicit, bool* aResult) { if (WebExtensionPolicy* policy = GetByID(aAddonId)) { *aResult = policy->CanAccessURI(aURI, aExplicit); return NS_OK; } return NS_ERROR_INVALID_ARG; } nsresult ExtensionPolicyService::GetExtensionName(const nsAString& aAddonId, nsAString& aName) { if (WebExtensionPolicy* policy = GetByID(aAddonId)) { aName.Assign(policy->Name()); return NS_OK; } return NS_ERROR_INVALID_ARG; } nsresult ExtensionPolicyService::ExtensionURILoadableByAnyone(nsIURI* aURI, bool* aResult) { URLInfo url(aURI); if (WebExtensionPolicy* policy = GetByURL(url)) { *aResult = policy->IsPathWebAccessible(url.FilePath()); return NS_OK; } return NS_ERROR_INVALID_ARG; } nsresult ExtensionPolicyService::ExtensionURIToAddonId(nsIURI* aURI, nsAString& aResult) { if (WebExtensionPolicy* policy = GetByURL(aURI)) { policy->GetId(aResult); } else { aResult.SetIsVoid(true); } return NS_OK; } NS_IMPL_CYCLE_COLLECTION(ExtensionPolicyService, mExtensions, mExtensionHosts, mObservers) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPolicyService) NS_INTERFACE_MAP_ENTRY(nsIAddonPolicyService) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) NS_INTERFACE_MAP_ENTRY(nsIMemoryReporter) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAddonPolicyService) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPolicyService) NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionPolicyService) } // namespace mozilla