tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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