ServiceWorkerUtils.cpp (15253B)
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/. */ 6 7 #include "ServiceWorkerUtils.h" 8 9 #include "mozilla/BasePrincipal.h" 10 #include "mozilla/ErrorResult.h" 11 #include "mozilla/LoadInfo.h" 12 #include "mozilla/Preferences.h" 13 #include "mozilla/StaticPrefs_dom.h" 14 #include "mozilla/StaticPrefs_extensions.h" 15 #include "mozilla/dom/BrowsingContext.h" 16 #include "mozilla/dom/ClientIPCTypes.h" 17 #include "mozilla/dom/ClientInfo.h" 18 #include "mozilla/dom/Document.h" 19 #include "mozilla/dom/Navigator.h" 20 #include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" 21 #include "mozilla/dom/ServiceWorkerRegistrarTypes.h" 22 #include "mozilla/dom/WorkerPrivate.h" 23 #include "mozilla/dom/WorkerRunnable.h" 24 #include "nsCOMPtr.h" 25 #include "nsContentPolicyUtils.h" 26 #include "nsIContentSecurityPolicy.h" 27 #include "nsIGlobalObject.h" 28 #include "nsIPrincipal.h" 29 #include "nsIURL.h" 30 #include "nsPrintfCString.h" 31 32 namespace mozilla::dom { 33 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(); 39 } 40 return false; 41 } 42 if (WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate()) { 43 return workerPrivate->ServiceWorkersTestingInWindow(); 44 } 45 return false; 46 } 47 48 bool ServiceWorkersEnabled(JSContext* aCx, JSObject* aGlobal) { 49 if (!StaticPrefs::dom_serviceWorkers_enabled()) { 50 return false; 51 } 52 53 // xpc::CurrentNativeGlobal below requires rooting 54 JS::Rooted<JSObject*> jsGlobal(aCx, aGlobal); 55 nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); 56 57 if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) { 58 // Only support ServiceWorkers in Private Browsing Mode (PBM) if Cache API 59 // and ServiceWorkers are enabled. We'll get weird errors without Cache 60 // API. 61 if (principal->GetIsInPrivateBrowsing() && 62 !(StaticPrefs::dom_cache_privateBrowsing_enabled() && 63 StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled())) { 64 return false; 65 } 66 67 // Allow a webextension principal to register a service worker script with 68 // a moz-extension url only if 'extensions.service_worker_register.allowed' 69 // is true. 70 if (!StaticPrefs::extensions_serviceWorkerRegister_allowed()) { 71 if (principal->GetIsAddonOrExpandedAddonPrincipal()) { 72 return false; 73 } 74 } 75 } 76 77 if (IsSecureContextOrObjectIsFromSecureContext(aCx, jsGlobal)) { 78 return true; 79 } 80 81 return StaticPrefs::dom_serviceWorkers_testing_enabled() || 82 IsServiceWorkersTestingEnabledInGlobal(jsGlobal); 83 } 84 85 bool ServiceWorkersStorageAllowedForGlobal(nsIGlobalObject* aGlobal) { 86 Maybe<ClientInfo> clientInfo = aGlobal->GetClientInfo(); 87 nsICookieJarSettings* cookieJarSettings = aGlobal->GetCookieJarSettings(); 88 nsIPrincipal* principal = aGlobal->PrincipalOrNull(); 89 90 if (NS_WARN_IF(clientInfo.isNothing() || !cookieJarSettings || !principal)) { 91 return false; 92 } 93 94 // Note that while we could call GetClientState on the global and it has a 95 // StorageAccess value, for non-fully active Window Clients, the storage 96 // access value is set to eDeny when snapshotted so we must not use it because 97 // this method may be called before a window becomes fully active. 98 auto storageAllowed = aGlobal->GetStorageAccess(); 99 100 // Allow access if: 101 // - Storage access is explicitly granted. 102 // - We are in private browsing and ServiceWorkers is allowed in PBM. Note 103 // that we will also potentially partition in PBM, so we have to do a 104 // separate PBM check in the partitioned case. 105 // - Partitioned access is granted and partitioning is enabled, plus if our 106 // principal is in PBM that ServiceWorkers are enabled in PBM. 107 return (storageAllowed == StorageAccess::eAllow || 108 (storageAllowed == StorageAccess::ePrivateBrowsing && 109 StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()) || 110 (ShouldPartitionStorage(storageAllowed) && 111 StaticPrefs::privacy_partition_serviceWorkers() && 112 StoragePartitioningEnabled(storageAllowed, cookieJarSettings) && 113 (!principal->GetIsInPrivateBrowsing() || 114 StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()))); 115 } 116 117 bool ServiceWorkersStorageAllowedForClient( 118 const ClientInfoAndState& aInfoAndState) { 119 ClientInfo info(aInfoAndState.info()); 120 ClientState state(ClientState::FromIPC(aInfoAndState.state())); 121 122 auto storageAllowed = state.GetStorageAccess(); 123 // This is the same check as in ServiceWorkersStorageAllowedForGlobal except 124 // that because we have no access to a cookie-jar we can't call 125 // StoragePartitioningEnabled. This isn't a concern in this case because any 126 // partitioning will already be baked into our principal. 127 return (storageAllowed == StorageAccess::eAllow || 128 (storageAllowed == StorageAccess::ePrivateBrowsing && 129 StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()) || 130 (ShouldPartitionStorage(storageAllowed) && 131 StaticPrefs::privacy_partition_serviceWorkers() && 132 /* note: no call to StoragePartitioningEnabled here */ 133 (!info.IsPrivateBrowsing() || 134 StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()))); 135 } 136 137 bool ServiceWorkerRegistrationDataIsValid( 138 const ServiceWorkerRegistrationData& aData) { 139 return !aData.scope().IsEmpty() && !aData.currentWorkerURL().IsEmpty() && 140 !aData.cacheName().IsEmpty(); 141 } 142 143 class WorkerCheckMayLoadSyncRunnable final : public WorkerMainThreadRunnable { 144 public: 145 explicit WorkerCheckMayLoadSyncRunnable( 146 std::function<void(ErrorResult&)>&& aCheckFunc) 147 : WorkerMainThreadRunnable(GetCurrentThreadWorkerPrivate(), 148 "WorkerCheckMayLoadSyncRunnable"_ns), 149 mCheckFunc(aCheckFunc) {} 150 151 bool MainThreadRun() override { 152 ErrorResult localResult; 153 mCheckFunc(localResult); 154 mRv = CopyableErrorResult(std::move(localResult)); 155 return true; 156 } 157 158 void PropagateErrorResult(ErrorResult& aOutRv) { 159 aOutRv = ErrorResult(std::move(mRv)); 160 } 161 162 private: 163 std::function<void(ErrorResult&)> mCheckFunc; 164 CopyableErrorResult mRv; 165 }; 166 167 namespace { 168 169 void CheckForSlashEscapedCharsInPath(nsIURI* aURI, const char* aURLDescription, 170 ErrorResult& aRv) { 171 MOZ_ASSERT(aURI); 172 173 // A URL that can't be downcast to a standard URL is an invalid URL and should 174 // be treated as such and fail with SecurityError. 175 nsCOMPtr<nsIURL> url(do_QueryInterface(aURI)); 176 if (NS_WARN_IF(!url)) { 177 // This really should not happen, since the caller checks that we 178 // have an http: or https: URL! 179 aRv.ThrowInvalidStateError("http: or https: URL without a concept of path"); 180 return; 181 } 182 183 nsAutoCString path; 184 nsresult rv = url->GetFilePath(path); 185 if (NS_WARN_IF(NS_FAILED(rv))) { 186 // Again, should not happen. 187 aRv.ThrowInvalidStateError("http: or https: URL without a concept of path"); 188 return; 189 } 190 191 ToLowerCase(path); 192 if (path.Find("%2f") != kNotFound || path.Find("%5c") != kNotFound) { 193 nsPrintfCString err("%s contains %%2f or %%5c", aURLDescription); 194 aRv.ThrowTypeError(err); 195 } 196 } 197 198 // Helper to take a lambda and, if we are already on the main thread, run it 199 // right now on the main thread, otherwise we use the 200 // WorkerCheckMayLoadSyncRunnable which spins a sync loop and run that on the 201 // main thread. When Bug 1901387 makes it possible to run CheckMayLoad logic 202 // on worker threads, this helper can be removed and the lambda flattened. 203 // 204 // This method takes an ErrorResult to pass as an argument to the lambda because 205 // the ErrorResult will also be used to capture dispatch failures. 206 void CheckMayLoadOnMainThread(ErrorResult& aRv, 207 std::function<void(ErrorResult&)>&& aCheckFunc) { 208 if (NS_IsMainThread()) { 209 aCheckFunc(aRv); 210 return; 211 } 212 213 RefPtr<WorkerCheckMayLoadSyncRunnable> runnable = 214 new WorkerCheckMayLoadSyncRunnable(std::move(aCheckFunc)); 215 runnable->Dispatch(GetCurrentThreadWorkerPrivate(), Canceling, aRv); 216 if (aRv.Failed()) { 217 return; 218 } 219 runnable->PropagateErrorResult(aRv); 220 } 221 222 } // anonymous namespace 223 224 void ServiceWorkerScopeAndScriptAreValid(const ClientInfo& aClientInfo, 225 nsIURI* aScopeURI, nsIURI* aScriptURI, 226 ErrorResult& aRv, 227 nsIGlobalObject* aGlobalForReporting) { 228 MOZ_DIAGNOSTIC_ASSERT(aScopeURI); 229 MOZ_DIAGNOSTIC_ASSERT(aScriptURI); 230 231 auto principalOrErr = aClientInfo.GetPrincipal(); 232 if (NS_WARN_IF(principalOrErr.isErr())) { 233 aRv.ThrowInvalidStateError("Can't make security decisions about Client"); 234 return; 235 } 236 237 auto hasHTTPScheme = [](nsIURI* aURI) -> bool { 238 return net::SchemeIsHttpOrHttps(aURI); 239 }; 240 auto hasMozExtScheme = [](nsIURI* aURI) -> bool { 241 return aURI->SchemeIs("moz-extension"); 242 }; 243 244 nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); 245 246 auto isExtension = principal->GetIsAddonOrExpandedAddonPrincipal(); 247 auto hasValidURISchemes = !isExtension ? hasHTTPScheme : hasMozExtScheme; 248 249 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 3. 250 if (!hasValidURISchemes(aScriptURI)) { 251 auto message = !isExtension 252 ? "Script URL's scheme is not 'http' or 'https'"_ns 253 : "Script URL's scheme is not 'moz-extension'"_ns; 254 aRv.ThrowTypeError(message); 255 return; 256 } 257 258 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 4. 259 CheckForSlashEscapedCharsInPath(aScriptURI, "script URL", aRv); 260 if (NS_WARN_IF(aRv.Failed())) { 261 return; 262 } 263 264 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 8. 265 if (!hasValidURISchemes(aScopeURI)) { 266 auto message = !isExtension 267 ? "Scope URL's scheme is not 'http' or 'https'"_ns 268 : "Scope URL's scheme is not 'moz-extension'"_ns; 269 aRv.ThrowTypeError(message); 270 return; 271 } 272 273 // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 9. 274 CheckForSlashEscapedCharsInPath(aScopeURI, "scope URL", aRv); 275 if (NS_WARN_IF(aRv.Failed())) { 276 return; 277 } 278 279 // The refs should really be empty coming in here, but if someone 280 // injects bad data into IPC, who knows. So let's revalidate that. 281 nsAutoCString ref; 282 (void)aScopeURI->GetRef(ref); 283 if (NS_WARN_IF(!ref.IsEmpty())) { 284 aRv.ThrowSecurityError("Non-empty fragment on scope URL"); 285 return; 286 } 287 288 (void)aScriptURI->GetRef(ref); 289 if (NS_WARN_IF(!ref.IsEmpty())) { 290 aRv.ThrowSecurityError("Non-empty fragment on script URL"); 291 return; 292 } 293 294 // CSP reporting on the main thread relies on the document node. 295 Document* maybeDoc = nullptr; 296 // CSP reporting for the worker relies on a helper listener. 297 nsCOMPtr<nsICSPEventListener> cspListener; 298 if (aGlobalForReporting) { 299 if (auto* win = aGlobalForReporting->GetAsInnerWindow()) { 300 maybeDoc = win->GetExtantDoc(); 301 if (!maybeDoc) { 302 aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); 303 return; 304 } 305 // LoadInfo has assertions about the Principal passed to it being the 306 // same object as the doc NodePrincipal(), so clobber principal to be 307 // that rather than the Principal we pulled out of the ClientInfo. 308 principal = maybeDoc->NodePrincipal(); 309 } else if (auto* wp = GetCurrentThreadWorkerPrivate()) { 310 cspListener = wp->CSPEventListener(); 311 } 312 } 313 314 // If this runs on the main thread, it is done synchronously. On workers all 315 // the references are safe due to the use of a sync runnable that blocks 316 // execution of the worker. The caveat is that control runnables can run 317 // while the syncloop spins and these can cause a worker global to start dying 318 // and WorkerRefs to be notified. However, GlobalTeardownObservers will only 319 // be torn down when the stack completely unwinds and no syncloops are on the 320 // stack. 321 CheckMayLoadOnMainThread(aRv, [&](ErrorResult& aResult) { 322 nsresult rv = principal->CheckMayLoadWithReporting( 323 aScopeURI, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */); 324 if (NS_WARN_IF(NS_FAILED(rv))) { 325 aResult.ThrowSecurityError("Scope URL is not same-origin with Client"); 326 return; 327 } 328 329 rv = principal->CheckMayLoadWithReporting( 330 aScriptURI, false /* allowIfInheritsPrincipal */, 331 0 /* innerWindowID */); 332 if (NS_WARN_IF(NS_FAILED(rv))) { 333 aResult.ThrowSecurityError("Script URL is not same-origin with Client"); 334 return; 335 } 336 337 // We perform a CSP check where the check will retrieve the CSP from the 338 // ClientInfo and validate worker-src directives or its fallbacks 339 // (https://w3c.github.io/webappsec-csp/#directive-worker-src). 340 // 341 // https://w3c.github.io/webappsec-csp/#fetch-integration explains how CSP 342 // integrates with fetch (although exact step numbers are currently out of 343 // sync). Specifically main fetch 344 // (https://fetch.spec.whatwg.org/#concept-main-fetch) does report-only 345 // checks in step 4, checks for request blocks in step 7, and response 346 // blocks in step 19. 347 // 348 // We are performing this check prior to our use of fetch due to asymmetries 349 // about application of CSP raised in Bug 1455077 and in more detail in the 350 // still-open https://github.com/w3c/ServiceWorker/issues/755. 351 // 352 // Also note that while fetch explicitly returns network errors for CSP, our 353 // logic here (and the CheckMayLoad calls above) corresponds to the steps of 354 // the register (https://w3c.github.io/ServiceWorker/#register-algorithm) 355 // which explicitly throws a SecurityError. 356 Result<RefPtr<net::LoadInfo>, nsresult> maybeLoadInfo = 357 net::LoadInfo::Create( 358 principal, // loading principal 359 principal, // triggering principal 360 maybeDoc, // loading node 361 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, 362 nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, Some(aClientInfo)); 363 if (NS_WARN_IF(maybeLoadInfo.isErr())) { 364 aResult.ThrowSecurityError("Script URL is not allowed by policy."); 365 return; 366 } 367 RefPtr<net::LoadInfo> secCheckLoadInfo = maybeLoadInfo.unwrap(); 368 369 if (cspListener) { 370 rv = secCheckLoadInfo->SetCspEventListener(cspListener); 371 if (NS_WARN_IF(NS_FAILED(rv))) { 372 aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); 373 return; 374 } 375 } 376 377 // Check content policy. 378 int16_t decision = nsIContentPolicy::ACCEPT; 379 rv = NS_CheckContentLoadPolicy(aScriptURI, secCheckLoadInfo, &decision); 380 if (NS_FAILED(rv) || NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { 381 aResult.ThrowSecurityError("Script URL is not allowed by policy."); 382 return; 383 } 384 }); 385 } 386 387 } // namespace mozilla::dom