From 8140fd1dc101255e6aae3707ccddbdaaa5130626 Mon Sep 17 00:00:00 2001 From: Perry Jiang Date: Thu, 24 Oct 2019 15:48:50 +0000 Subject: [PATCH] Bug 1575090 - set COEP for all workers and enforce it when loading Dedicated Workers r=asuth Differential Revision: https://phabricator.services.mozilla.com/D46177 --HG-- extra : moz-landing-system : lando --- dom/workers/ScriptLoader.cpp | 145 ++++++++++++++++++++++++++++------ dom/workers/WorkerPrivate.cpp | 79 ++++++++++++++++++ dom/workers/WorkerPrivate.h | 49 +++++++++++- 3 files changed, 247 insertions(+), 26 deletions(-) diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp index 30c1cb5a4b81..3939c1653ae0 100644 --- a/dom/workers/ScriptLoader.cpp +++ b/dom/workers/ScriptLoader.cpp @@ -71,6 +71,8 @@ #include "mozilla/dom/SRILogHelper.h" #include "mozilla/dom/ServiceWorkerBinding.h" #include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_security.h" #include "mozilla/UniquePtr.h" @@ -491,8 +493,8 @@ class CacheScriptLoader final : public PromiseNativeHandler, ScriptLoadInfo& mLoadInfo; uint32_t mIndex; - RefPtr mRunnable; - bool mIsWorkerScript; + const RefPtr mRunnable; + const bool mIsWorkerScript; bool mFailed; const ServiceWorkerState mState; nsCOMPtr mPump; @@ -567,13 +569,86 @@ class LoaderListener final : public nsIStreamLoaderObserver, NS_IMPL_ISUPPORTS(LoaderListener, nsIStreamLoaderObserver, nsIRequestObserver) +class ScriptResponseHeaderProcessor final : public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + + ScriptResponseHeaderProcessor(WorkerPrivate* aWorkerPrivate, + bool aIsMainScript) + : mWorkerPrivate(aWorkerPrivate), mIsMainScript(aIsMainScript) { + AssertIsOnMainThread(); + } + + NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override { + nsresult rv = ProcessCrossOriginEmbedderPolicyHeader(aRequest); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + } + + return rv; + } + + NS_IMETHOD OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) override { + MOZ_DIAGNOSTIC_ASSERT_IF(NS_SUCCEEDED(aStatusCode), + mWorkerPrivate->GetEmbedderPolicy().isSome()); + + return NS_OK; + } + + static nsresult ProcessCrossOriginEmbedderPolicyHeader( + WorkerPrivate* aWorkerPrivate, + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy, bool aIsMainScript) { + MOZ_ASSERT(aWorkerPrivate); + + if (aIsMainScript) { + MOZ_TRY(aWorkerPrivate->SetEmbedderPolicy(aPolicy)); + } else if (!aWorkerPrivate->MatchEmbedderPolicy(aPolicy)) { + return NS_ERROR_BLOCKED_BY_POLICY; + } + + return NS_OK; + } + + private: + ~ScriptResponseHeaderProcessor() = default; + + nsresult ProcessCrossOriginEmbedderPolicyHeader(nsIRequest* aRequest) { + MOZ_ASSERT_IF(!mIsMainScript, mWorkerPrivate->GetEmbedderPolicy().isSome()); + + nsCOMPtr httpChannel = do_QueryInterface(aRequest); + + // NOTE: the spec doesn't say what to do with non-HTTP workers. + // See: https://github.com/whatwg/html/issues/4916 + if (!httpChannel) { + if (mIsMainScript) { + mWorkerPrivate->InheritOwnerEmbedderPolicyOrNull(aRequest); + } + + return NS_OK; + } + + nsILoadInfo::CrossOriginEmbedderPolicy coep; + MOZ_TRY(httpChannel->GetResponseEmbedderPolicy(&coep)); + + return ProcessCrossOriginEmbedderPolicyHeader(mWorkerPrivate, coep, + mIsMainScript); + } + + WorkerPrivate* const mWorkerPrivate; + const bool mIsMainScript; +}; + +NS_IMPL_ISUPPORTS(ScriptResponseHeaderProcessor, nsIRequestObserver); + class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { friend class ScriptExecutorRunnable; friend class CachePromiseHandler; friend class CacheScriptLoader; friend class LoaderListener; - WorkerPrivate* mWorkerPrivate; + WorkerPrivate* const mWorkerPrivate; UniquePtr mOriginStack; nsString mOriginStackJSON; nsCOMPtr mSyncLoopTarget; @@ -581,7 +656,7 @@ class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { RefPtr mCacheCreator; Maybe mClientInfo; Maybe mController; - bool mIsMainScript; + const bool mIsMainScript; WorkerScriptType mWorkerScriptType; bool mCanceledMainThread; ErrorResult& mRv; @@ -680,13 +755,22 @@ class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { } nsresult OnStartRequest(nsIRequest* aRequest, uint32_t aIndex) { + nsresult rv = OnStartRequestInternal(aRequest, aIndex); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + } + + return rv; + } + + nsresult OnStartRequestInternal(nsIRequest* aRequest, uint32_t aIndex) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); // If one load info cancels or hits an error, it can race with the start // callback coming from another load info. if (mCanceledMainThread || !mCacheCreator) { - aRequest->Cancel(NS_ERROR_FAILURE); return NS_ERROR_FAILURE; } @@ -715,7 +799,6 @@ class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { nsTArray{NS_ConvertUTF8toUTF16(scope), NS_ConvertUTF8toUTF16(mimeType), loadInfo.mURL}); - channel->Cancel(NS_ERROR_DOM_NETWORK_ERR); return NS_ERROR_DOM_NETWORK_ERR; } } @@ -745,19 +828,11 @@ class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { NS_ASSERTION(ssm, "Should never be null!"); nsCOMPtr channelPrincipal; - nsresult rv = ssm->GetChannelResultPrincipal( - channel, getter_AddRefs(channelPrincipal)); - if (NS_WARN_IF(NS_FAILED(rv))) { - channel->Cancel(rv); - return rv; - } + MOZ_TRY(ssm->GetChannelResultPrincipal(channel, + getter_AddRefs(channelPrincipal))); UniquePtr principalInfo(new PrincipalInfo()); - rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get()); - if (NS_WARN_IF(NS_FAILED(rv))) { - channel->Cancel(rv); - return rv; - } + MOZ_TRY(PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get())); ir->SetPrincipalInfo(std::move(principalInfo)); ir->Headers()->FillResponseHeaders(loadInfo.mChannel); @@ -781,9 +856,7 @@ class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { mCacheCreator->Cache_()->Put(jsapi.cx(), request, *response, error); error.WouldReportJSException(); if (NS_WARN_IF(error.Failed())) { - nsresult rv = error.StealNSResult(); - channel->Cancel(rv); - return rv; + return error.StealNSResult(); } RefPtr promiseHandler = @@ -1016,10 +1089,17 @@ class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { // where to put the result. RefPtr listener = new LoaderListener(this, aIndex); - // We don't care about progress so just use the simple stream loader for - // OnStreamComplete notification only. + RefPtr headerProcessor = nullptr; + + // For each debugger script, a non-debugger script load of the same script + // should have occured prior that processed the headers. + if (!IsDebuggerScript()) { + headerProcessor = MakeRefPtr( + mWorkerPrivate, mIsMainScript); + } + nsCOMPtr loader; - rv = NS_NewStreamLoader(getter_AddRefs(loader), listener); + rv = NS_NewStreamLoader(getter_AddRefs(loader), listener, headerProcessor); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } @@ -1749,6 +1829,25 @@ void CacheScriptLoader::ResolvedCallback(JSContext* aCx, headers->Get(NS_LITERAL_CSTRING("referrer-policy"), mReferrerPolicyHeaderValue, IgnoreErrors()); + nsAutoCString coepHeader; + headers->Get(NS_LITERAL_CSTRING("cross-origin-embedder-policy"), coepHeader, + IgnoreErrors()); + + nsILoadInfo::CrossOriginEmbedderPolicy coep = + nsILoadInfo::EMBEDDER_POLICY_NULL; + + if (coepHeader.EqualsLiteral("require-corp")) { + coep = nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP; + } + + rv = ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + mRunnable->mWorkerPrivate, coep, mRunnable->mIsMainScript); + + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + nsCOMPtr inputStream; response->GetBody(getter_AddRefs(inputStream)); mChannelInfo = response->GetChannelInfo(); diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp index 65887a252ce7..486ac42c956d 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -15,6 +15,7 @@ #include "js/SourceText.h" #include "MessageEventRunnable.h" #include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_browser.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/BlobURLProtocolHandler.h" #include "mozilla/dom/CallbackDebuggerNotification.h" @@ -4992,6 +4993,84 @@ bool WorkerPrivate::CanShareMemory(const nsID& aAgentClusterId) { nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP; } +Maybe WorkerPrivate::GetEmbedderPolicy() + const { + MOZ_ASSERT(NS_IsMainThread()); + + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return Some(nsILoadInfo::EMBEDDER_POLICY_NULL); + } + + return mEmbedderPolicy; +} + +Result WorkerPrivate::SetEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return Ok(); + } + + if (GetOwnerEmbedderPolicy().valueOr(aPolicy) != aPolicy) { + return Err(NS_ERROR_BLOCKED_BY_POLICY); + } + + mEmbedderPolicy.emplace(aPolicy); + + return Ok(); +} + +void WorkerPrivate::InheritOwnerEmbedderPolicyOrNull(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + + auto coep = GetOwnerEmbedderPolicy(); + + if (coep.isSome()) { + nsCOMPtr channel = do_QueryInterface(aRequest); + MOZ_ASSERT(channel); + + nsCOMPtr scriptURI; + MOZ_ALWAYS_SUCCEEDS(channel->GetURI(getter_AddRefs(scriptURI))); + + bool isLocalScriptURI = false; + MOZ_ALWAYS_SUCCEEDS(NS_URIChainHasFlags( + scriptURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, + &isLocalScriptURI)); + + MOZ_RELEASE_ASSERT(isLocalScriptURI); + } + + mEmbedderPolicy.emplace(coep.valueOr(nsILoadInfo::EMBEDDER_POLICY_NULL)); +} + +bool WorkerPrivate::MatchEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) const { + MOZ_ASSERT(NS_IsMainThread()); + + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return true; + } + + return mEmbedderPolicy.value() == aPolicy; +} + +Maybe +WorkerPrivate::GetOwnerEmbedderPolicy() const { + MOZ_ASSERT(NS_IsMainThread()); + + if (GetParent()) { + return GetParent()->GetEmbedderPolicy(); + } + + if (GetWindow() && GetWindow()->GetBrowsingContext()) { + return Some(GetWindow()->GetBrowsingContext()->GetEmbedderPolicy()); + } + + return Nothing(); +} + NS_IMPL_ADDREF(WorkerPrivate::EventTarget) NS_IMPL_RELEASE(WorkerPrivate::EventTarget) diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index 6cbf39bb9372..9b90f91943d6 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -7,18 +7,23 @@ #ifndef mozilla_dom_workers_workerprivate_h__ #define mozilla_dom_workers_workerprivate_h__ +#include "MainThreadUtils.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerStatus.h" +#include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/CondVar.h" #include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Maybe.h" #include "mozilla/MozPromise.h" #include "mozilla/RelativeTimeline.h" +#include "mozilla/Result.h" #include "mozilla/StorageAccess.h" #include "mozilla/ThreadSafeWeakPtr.h" #include "nsContentUtils.h" #include "nsIContentSecurityPolicy.h" #include "nsIEventTarget.h" +#include "nsILoadInfo.h" #include "nsTObserverArray.h" #include "js/ContextOptions.h" @@ -712,7 +717,7 @@ class WorkerPrivate : public RelativeTimeline { return mLoadInfo.mChannel.forget(); } - nsPIDOMWindowInner* GetWindow() { + nsPIDOMWindowInner* GetWindow() const { AssertIsOnMainThread(); return mLoadInfo.mWindow; } @@ -902,6 +907,34 @@ class WorkerPrivate : public RelativeTimeline { return mAgentClusterOpenerPolicy; } + /** + * COEP Methods + * + * If browser.tabs.remote.useCrossOriginEmbedderPolicy=false, these methods + * will, depending on the return type, return a value that will avoid + * assertion failures or a value that won't block loads. + */ + + Maybe GetEmbedderPolicy() const; + + // Fails if a policy has already been set or if `aPolicy` violates the owner's + // policy, if an owner exists. + mozilla::Result SetEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy); + + // `aRequest` is the request loading the worker and must be QI-able to + // `nsIChannel*`. It's used to verify that the worker can indeed inherit its + // owner's COEP (when an owner exists). + // + // TODO: remove `aRequest`; currently, it's required because instances may not + // always know its final, resolved script URL or have access internally to + // `aRequest`. + void InheritOwnerEmbedderPolicyOrNull(nsIRequest* aRequest); + + // Requires a policy to already have been set. + bool MatchEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) const; + private: WorkerPrivate( WorkerPrivate* aParent, const nsAString& aScriptURL, bool aIsChromeWorker, @@ -997,6 +1030,8 @@ class WorkerPrivate : public RelativeTimeline { // executed. void DispatchCancelingRunnable(); + Maybe GetOwnerEmbedderPolicy() const; + class EventTarget; friend class EventTarget; friend class AutoSyncLoopHolder; @@ -1011,9 +1046,9 @@ class WorkerPrivate : public RelativeTimeline { SharedMutex mMutex; mozilla::CondVar mCondVar; - WorkerPrivate* mParent; + WorkerPrivate* const mParent; - nsString mScriptURL; + const nsString mScriptURL; // This is the worker name for shared workers and dedicated workers. nsString mWorkerName; @@ -1220,6 +1255,14 @@ class WorkerPrivate : public RelativeTimeline { // This is used to check if it's allowed to share the memory across the agent // cluster. const nsILoadInfo::CrossOriginOpenerPolicy mAgentClusterOpenerPolicy; + + // Member variable of this class rather than the worker global scope because + // it's received on the main thread, but the global scope is thread-bound + // to the worker thread, so storing the value in the global scope would + // involve sacrificing the thread-bound-ness or using a WorkerRunnable, and + // there isn't a strong reason to store it on the global scope other than + // better consistency with the COEP spec. + Maybe mEmbedderPolicy; }; class AutoSyncLoopHolder {