1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ServiceWorkerUtils.h"
9 #include "nsContentPolicyUtils.h"
11 #include "mozilla/BasePrincipal.h"
12 #include "mozilla/ErrorResult.h"
13 #include "mozilla/LoadInfo.h"
14 #include "mozilla/Preferences.h"
15 #include "mozilla/StaticPrefs_dom.h"
16 #include "mozilla/StaticPrefs_extensions.h"
17 #include "mozilla/dom/BrowsingContext.h"
18 #include "mozilla/dom/ClientInfo.h"
19 #include "mozilla/dom/Document.h"
20 #include "mozilla/dom/Navigator.h"
21 #include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
22 #include "mozilla/dom/ServiceWorkerRegistrarTypes.h"
23 #include "mozilla/dom/WorkerPrivate.h"
24 #include "mozilla/dom/WorkerRunnable.h"
26 #include "nsIContentSecurityPolicy.h"
27 #include "nsIGlobalObject.h"
28 #include "nsIPrincipal.h"
30 #include "nsPrintfCString.h"
32 namespace mozilla::dom
{
34 static bool IsServiceWorkersTestingEnabledInGlobal(JSObject
* const aGlobal
) {
35 if (const nsCOMPtr
<nsPIDOMWindowInner
> innerWindow
=
36 Navigator::GetWindowFromGlobal(aGlobal
)) {
37 if (auto* bc
= innerWindow
->GetBrowsingContext()) {
38 return bc
->Top()->ServiceWorkersTestingEnabled();
42 if (WorkerPrivate
* workerPrivate
= GetCurrentThreadWorkerPrivate()) {
43 return workerPrivate
->ServiceWorkersTestingInWindow();
48 bool ServiceWorkersEnabled(JSContext
* aCx
, JSObject
* aGlobal
) {
49 if (!StaticPrefs::dom_serviceWorkers_enabled()) {
53 // xpc::CurrentNativeGlobal below requires rooting
54 JS::Rooted
<JSObject
*> jsGlobal(aCx
, aGlobal
);
55 nsIGlobalObject
* global
= xpc::CurrentNativeGlobal(aCx
);
57 if (const nsCOMPtr
<nsIPrincipal
> principal
= global
->PrincipalOrNull()) {
58 // ServiceWorkers are currently not available in PrivateBrowsing.
59 // Bug 1320796 will change this.
60 if (principal
->GetIsInPrivateBrowsing()) {
64 // Allow a webextension principal to register a service worker script with
65 // a moz-extension url only if 'extensions.service_worker_register.allowed'
67 if (!StaticPrefs::extensions_serviceWorkerRegister_allowed()) {
68 if (principal
->GetIsAddonOrExpandedAddonPrincipal()) {
74 if (IsSecureContextOrObjectIsFromSecureContext(aCx
, jsGlobal
)) {
78 return StaticPrefs::dom_serviceWorkers_testing_enabled() ||
79 IsServiceWorkersTestingEnabledInGlobal(jsGlobal
);
82 bool ServiceWorkerRegistrationDataIsValid(
83 const ServiceWorkerRegistrationData
& aData
) {
84 return !aData
.scope().IsEmpty() && !aData
.currentWorkerURL().IsEmpty() &&
85 !aData
.cacheName().IsEmpty();
88 class WorkerCheckMayLoadSyncRunnable final
: public WorkerMainThreadRunnable
{
90 WorkerCheckMayLoadSyncRunnable(std::function
<void(ErrorResult
&)>&& aCheckFunc
,
92 : WorkerMainThreadRunnable(GetCurrentThreadWorkerPrivate(),
93 "WorkerCheckMayLoadSyncRunnable"_ns
),
94 mCheckFunc(aCheckFunc
),
97 bool MainThreadRun() override
{
103 std::function
<void(ErrorResult
&)> mCheckFunc
;
104 // This reference is safe because we are a synchronously dispatched runnable
105 // and while we expect the ErrorResult to be stack-allocated, our runnable
106 // holds that stack alive during the sync dispatch.
112 void CheckForSlashEscapedCharsInPath(nsIURI
* aURI
, const char* aURLDescription
,
116 // A URL that can't be downcast to a standard URL is an invalid URL and should
117 // be treated as such and fail with SecurityError.
118 nsCOMPtr
<nsIURL
> url(do_QueryInterface(aURI
));
119 if (NS_WARN_IF(!url
)) {
120 // This really should not happen, since the caller checks that we
121 // have an http: or https: URL!
122 aRv
.ThrowInvalidStateError("http: or https: URL without a concept of path");
127 nsresult rv
= url
->GetFilePath(path
);
128 if (NS_WARN_IF(NS_FAILED(rv
))) {
129 // Again, should not happen.
130 aRv
.ThrowInvalidStateError("http: or https: URL without a concept of path");
135 if (path
.Find("%2f") != kNotFound
|| path
.Find("%5c") != kNotFound
) {
136 nsPrintfCString
err("%s contains %%2f or %%5c", aURLDescription
);
137 aRv
.ThrowTypeError(err
);
141 // Helper to take a lambda and, if we are already on the main thread, run it
142 // right now on the main thread, otherwise we use the
143 // WorkerCheckMayLoadSyncRunnable which spins a sync loop and run that on the
144 // main thread. When Bug 1901387 makes it possible to run CheckMayLoad logic
145 // on worker threads, this helper can be removed and the lambda flattened.
147 // This method takes an ErrorResult to pass as an argument to the lambda because
148 // the ErrorResult will also be used to capture dispatch failures.
149 void CheckMayLoadOnMainThread(ErrorResult
& aRv
,
150 std::function
<void(ErrorResult
&)>&& aCheckFunc
) {
151 if (NS_IsMainThread()) {
156 RefPtr
<WorkerCheckMayLoadSyncRunnable
> runnable
=
157 new WorkerCheckMayLoadSyncRunnable(std::move(aCheckFunc
), aRv
);
158 runnable
->Dispatch(GetCurrentThreadWorkerPrivate(), Canceling
, aRv
);
161 } // anonymous namespace
163 void ServiceWorkerScopeAndScriptAreValid(const ClientInfo
& aClientInfo
,
164 nsIURI
* aScopeURI
, nsIURI
* aScriptURI
,
166 nsIGlobalObject
* aGlobalForReporting
) {
167 MOZ_DIAGNOSTIC_ASSERT(aScopeURI
);
168 MOZ_DIAGNOSTIC_ASSERT(aScriptURI
);
170 auto principalOrErr
= aClientInfo
.GetPrincipal();
171 if (NS_WARN_IF(principalOrErr
.isErr())) {
172 aRv
.ThrowInvalidStateError("Can't make security decisions about Client");
176 auto hasHTTPScheme
= [](nsIURI
* aURI
) -> bool {
177 return aURI
->SchemeIs("http") || aURI
->SchemeIs("https");
179 auto hasMozExtScheme
= [](nsIURI
* aURI
) -> bool {
180 return aURI
->SchemeIs("moz-extension");
183 nsCOMPtr
<nsIPrincipal
> principal
= principalOrErr
.unwrap();
185 auto isExtension
= principal
->GetIsAddonOrExpandedAddonPrincipal();
186 auto hasValidURISchemes
= !isExtension
? hasHTTPScheme
: hasMozExtScheme
;
188 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 3.
189 if (!hasValidURISchemes(aScriptURI
)) {
190 auto message
= !isExtension
191 ? "Script URL's scheme is not 'http' or 'https'"_ns
192 : "Script URL's scheme is not 'moz-extension'"_ns
;
193 aRv
.ThrowTypeError(message
);
197 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 4.
198 CheckForSlashEscapedCharsInPath(aScriptURI
, "script URL", aRv
);
199 if (NS_WARN_IF(aRv
.Failed())) {
203 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 8.
204 if (!hasValidURISchemes(aScopeURI
)) {
205 auto message
= !isExtension
206 ? "Scope URL's scheme is not 'http' or 'https'"_ns
207 : "Scope URL's scheme is not 'moz-extension'"_ns
;
208 aRv
.ThrowTypeError(message
);
212 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 9.
213 CheckForSlashEscapedCharsInPath(aScopeURI
, "scope URL", aRv
);
214 if (NS_WARN_IF(aRv
.Failed())) {
218 // The refs should really be empty coming in here, but if someone
219 // injects bad data into IPC, who knows. So let's revalidate that.
221 Unused
<< aScopeURI
->GetRef(ref
);
222 if (NS_WARN_IF(!ref
.IsEmpty())) {
223 aRv
.ThrowSecurityError("Non-empty fragment on scope URL");
227 Unused
<< aScriptURI
->GetRef(ref
);
228 if (NS_WARN_IF(!ref
.IsEmpty())) {
229 aRv
.ThrowSecurityError("Non-empty fragment on script URL");
233 // CSP reporting on the main thread relies on the document node.
234 Document
* maybeDoc
= nullptr;
235 // CSP reporting for the worker relies on a helper listener.
236 nsCOMPtr
<nsICSPEventListener
> cspListener
;
237 if (aGlobalForReporting
) {
238 if (auto* win
= aGlobalForReporting
->GetAsInnerWindow()) {
239 maybeDoc
= win
->GetExtantDoc();
241 aRv
.Throw(NS_ERROR_DOM_INVALID_STATE_ERR
);
244 // LoadInfo has assertions about the Principal passed to it being the
245 // same object as the doc NodePrincipal(), so clobber principal to be
246 // that rather than the Principal we pulled out of the ClientInfo.
247 principal
= maybeDoc
->NodePrincipal();
248 } else if (auto* wp
= GetCurrentThreadWorkerPrivate()) {
249 cspListener
= wp
->CSPEventListener();
253 // If this runs on the main thread, it is done synchronously. On workers all
254 // the references are safe due to the use of a sync runnable that blocks
255 // execution of the worker. The caveat is that control runnables can run
256 // while the syncloop spins and these can cause a worker global to start dying
257 // and WorkerRefs to be notified. However, GlobalTeardownObservers will only
258 // be torn down when the stack completely unwinds and no syncloops are on the
260 CheckMayLoadOnMainThread(aRv
, [&](ErrorResult
& aResult
) {
261 nsresult rv
= principal
->CheckMayLoadWithReporting(
262 aScopeURI
, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */);
263 if (NS_WARN_IF(NS_FAILED(rv
))) {
264 aResult
.ThrowSecurityError("Scope URL is not same-origin with Client");
268 rv
= principal
->CheckMayLoadWithReporting(
269 aScriptURI
, false /* allowIfInheritsPrincipal */,
270 0 /* innerWindowID */);
271 if (NS_WARN_IF(NS_FAILED(rv
))) {
272 aResult
.ThrowSecurityError("Script URL is not same-origin with Client");
276 // We perform a CSP check where the check will retrieve the CSP from the
277 // ClientInfo and validate worker-src directives or its fallbacks
278 // (https://w3c.github.io/webappsec-csp/#directive-worker-src).
280 // https://w3c.github.io/webappsec-csp/#fetch-integration explains how CSP
281 // integrates with fetch (although exact step numbers are currently out of
282 // sync). Specifically main fetch
283 // (https://fetch.spec.whatwg.org/#concept-main-fetch) does report-only
284 // checks in step 4, checks for request blocks in step 7, and response
285 // blocks in step 19.
287 // We are performing this check prior to our use of fetch due to asymmetries
288 // about application of CSP raised in Bug 1455077 and in more detail in the
289 // still-open https://github.com/w3c/ServiceWorker/issues/755.
291 // Also note that while fetch explicitly returns network errors for CSP, our
292 // logic here (and the CheckMayLoad calls above) corresponds to the steps of
293 // the register (https://w3c.github.io/ServiceWorker/#register-algorithm)
294 // which explicitly throws a SecurityError.
295 nsCOMPtr
<nsILoadInfo
> secCheckLoadInfo
= new mozilla::net::LoadInfo(
296 principal
, // loading principal
297 principal
, // triggering principal
298 maybeDoc
, // loading node
299 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
,
300 nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER
, Some(aClientInfo
));
303 rv
= secCheckLoadInfo
->SetCspEventListener(cspListener
);
304 if (NS_WARN_IF(NS_FAILED(rv
))) {
305 aRv
.Throw(NS_ERROR_DOM_INVALID_STATE_ERR
);
310 // Check content policy.
311 int16_t decision
= nsIContentPolicy::ACCEPT
;
312 rv
= NS_CheckContentLoadPolicy(aScriptURI
, secCheckLoadInfo
, &decision
);
313 if (NS_FAILED(rv
) || NS_WARN_IF(decision
!= nsIContentPolicy::ACCEPT
)) {
314 aResult
.ThrowSecurityError("Script URL is not allowed by policy.");
320 } // namespace mozilla::dom