Bug 1732919 - Use user activation for Storage Access API. r=edgar,anti-tracking-reviewers,timhuang
☠☠ backed out by ae6cc33d751b ☠ ☠
authorPaul Zuehlcke <pbz@mozilla.com>
Mon, 11 Oct 2021 14:54:49 +0000
changeset 595371 2c86a1d595b83492f8eb19b2208e98d77dc51f10
parent 595370 e15c1956a0ebadaf0e4e869eb8c7e24667d63359
child 595372 759d9b550cca9061128e98b24909c9547e02bc89
push id151230
push userpzuhlcke@mozilla.com
push dateMon, 11 Oct 2021 14:57:15 +0000
treeherderautoland@759d9b550cca [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedgar, anti-tracking-reviewers, timhuang
bugs1732919
milestone95.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1732919 - Use user activation for Storage Access API. r=edgar,anti-tracking-reviewers,timhuang Differential Revision: https://phabricator.services.mozilla.com/D127151
dom/base/Document.cpp
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -16574,54 +16574,66 @@ Document::GetContentBlockingEvents() {
 already_AddRefed<mozilla::dom::Promise> Document::RequestStorageAccess(
     mozilla::ErrorResult& aRv) {
   nsIGlobalObject* global = GetScopeObject();
   if (!global) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
-  // Propagate user input event handling to the resolve handler
-  RefPtr<Promise> promise =
-      Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
+  RefPtr<Promise> promise = Promise::Create(global, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
+  // Window doesn't have user activation, reject.
+  if (!this->HasValidTransientUserGestureActivation()) {
+    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+                                    nsLiteralCString("requestStorageAccess"),
+                                    this, nsContentUtils::eDOM_PROPERTIES,
+                                    "RequestStorageAccessUserGesture");
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
   nsCOMPtr<nsPIDOMWindowInner> inner = GetInnerWindow();
   if (!inner) {
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   // Step 1. If the document already has been granted access, resolve.
   RefPtr<nsGlobalWindowOuter> outer =
       nsGlobalWindowOuter::Cast(inner->GetOuterWindow());
   if (!outer) {
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   if (outer->IsStorageAccessPermissionGranted()) {
     promise->MaybeResolveWithUndefined();
     return promise.forget();
   }
 
   // Step 2. If the document has a null origin, reject.
   if (NodePrincipal()->GetIsNullPrincipal()) {
     nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
                                     nsLiteralCString("requestStorageAccess"),
                                     this, nsContentUtils::eDOM_PROPERTIES,
                                     "RequestStorageAccessNullPrincipal");
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   RefPtr<BrowsingContext> bc = GetBrowsingContext();
   if (!bc) {
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   // Only enforce third-party checks when there is a reason to enforce them.
   if (!CookieJarSettings()->GetRejectThirdPartyContexts()) {
     // Step 3. If the document's frame is the main frame, resolve.
     if (IsTopLevelContentDocument()) {
@@ -16634,22 +16646,24 @@ already_AddRefed<mozilla::dom::Promise> 
     // In fission, if the sub frame's origin differs from the main frame's
     // origin, they will be in different processes. We use IsInProcess()
     // check here to deterimine whether they have the same origin. In
     // non-fission mode, it is always in-process so we need to compare their
     // principals.
     if (bc->Top()->IsInProcess()) {
       nsCOMPtr<nsPIDOMWindowOuter> topOuter = bc->Top()->GetDOMWindow();
       if (!topOuter) {
+        this->ConsumeTransientUserGestureActivation();
         promise->MaybeRejectWithUndefined();
         return promise.forget();
       }
 
       nsCOMPtr<Document> topLevelDoc = topOuter->GetExtantDoc();
       if (!topLevelDoc) {
+        this->ConsumeTransientUserGestureActivation();
         promise->MaybeRejectWithUndefined();
         return promise.forget();
       }
 
       if (topLevelDoc->NodePrincipal()->Equals(NodePrincipal())) {
         promise->MaybeResolveWithUndefined();
         return promise.forget();
       }
@@ -16658,36 +16672,28 @@ already_AddRefed<mozilla::dom::Promise> 
 
   // Step 5. If the sub frame is not sandboxed, skip to step 7.
   // Step 6. If the sub frame doesn't have the token
   //         "allow-storage-access-by-user-activation", reject.
   if (StorageAccessSandboxed()) {
     nsContentUtils::ReportToConsole(
         nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
         this, nsContentUtils::eDOM_PROPERTIES, "RequestStorageAccessSandboxed");
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   // Step 7. If the sub frame's parent frame is not the top frame, reject.
   RefPtr<BrowsingContext> parentBC = bc->GetParent();
   if (parentBC && !parentBC->IsTopContent()) {
     nsContentUtils::ReportToConsole(
         nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
         this, nsContentUtils::eDOM_PROPERTIES, "RequestStorageAccessNested");
-    promise->MaybeRejectWithUndefined();
-    return promise.forget();
-  }
-
-  // Step 8. If the browser is not processing a user gesture, reject.
-  if (!UserActivation::IsHandlingUserInput()) {
-    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
-                                    nsLiteralCString("requestStorageAccess"),
-                                    this, nsContentUtils::eDOM_PROPERTIES,
-                                    "RequestStorageAccessUserGesture");
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   // Step 9. Check any additional rules that the browser has.
   //         Examples: skip-lists, on-device classification,
   //         user settings, anti-clickjacking heuristics, or prompting the
   //         user for explicit permission. Reject if some rule is not fulfilled.
@@ -16695,16 +16701,17 @@ already_AddRefed<mozilla::dom::Promise> 
     // Only do something special for third-party tracking content.
     uint32_t antiTrackingRejectedReason = 0;
     if (StorageDisabledByAntiTracking(this, nullptr,
                                       antiTrackingRejectedReason)) {
       // If storage is disabled because of a custom cookie permission for the
       // site, reject.
       if (antiTrackingRejectedReason ==
           nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) {
+        this->ConsumeTransientUserGestureActivation();
         promise->MaybeRejectWithUndefined();
         return promise.forget();
       }
 
       // Note: If this has returned true, the top-level document is guaranteed
       // to not be on the Content Blocking allow list.
       MOZ_ASSERT(!CookieJarSettings()->GetIsOnContentBlockingAllowList());
 
@@ -16789,27 +16796,33 @@ already_AddRefed<mozilla::dom::Promise> 
 
               sapr->RequestDelayedTask(
                   inner->EventTargetFor(TaskCategory::Other),
                   ContentPermissionRequestBase::DelayedTaskType::Request);
             });
 
         return p;
       };
+
+      // Consume user activation before entering the async part of this method.
+      // This prevents usage of other transient activation-gated APIs.
+      this->ConsumeTransientUserGestureActivation();
+
       ContentBlocking::AllowAccessFor(
           NodePrincipal(), bc, ContentBlockingNotifier::eStorageAccessAPI,
           performFinalChecks)
           ->Then(
               GetCurrentSerialEventTarget(), __func__,
-              [outer, promise] {
+              [self, outer, promise] {
                 // Step 10. Grant the document access to cookies and store
                 // that fact for
                 //          the purposes of future calls to
                 //          hasStorageAccess() and requestStorageAccess().
                 outer->SetStorageAccessPermissionGranted(true);
+                self->NotifyUserGestureActivation();
                 promise->MaybeResolveWithUndefined();
               },
               [outer, promise] {
                 outer->SetStorageAccessPermissionGranted(false);
                 promise->MaybeRejectWithUndefined();
               });
 
       return promise.forget();
@@ -16824,41 +16837,53 @@ already_AddRefed<mozilla::dom::Promise> 
 already_AddRefed<mozilla::dom::Promise> Document::RequestStorageAccessForOrigin(
     const nsAString& aThirdPartyOrigin, mozilla::ErrorResult& aRv) {
   nsIGlobalObject* global = GetScopeObject();
   if (!global) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
-  RefPtr<Promise> promise =
-      Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
+  RefPtr<Promise> promise = Promise::Create(global, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
+  // Window doesn't have user activation, reject.
+  if (!this->HasValidTransientUserGestureActivation()) {
+    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+                                    nsLiteralCString("requestStorageAccess"),
+                                    this, nsContentUtils::eDOM_PROPERTIES,
+                                    "RequestStorageAccessUserGesture");
+    promise->MaybeRejectWithUndefined();
+    return promise.forget();
+  }
+
   nsCOMPtr<nsPIDOMWindowInner> inner = GetInnerWindow();
   if (!inner) {
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   // We only allow request storage access for third-party origin from the
   // first-party context.
   if (AntiTrackingUtils::IsThirdPartyWindow(inner, nullptr)) {
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   // If the document has a null origin, reject.
   if (NodePrincipal()->GetIsNullPrincipal()) {
     nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
                                     nsLiteralCString("requestStorageAccess"),
                                     this, nsContentUtils::eDOM_PROPERTIES,
                                     "RequestStorageAccessNullPrincipal");
+    this->ConsumeTransientUserGestureActivation();
     promise->MaybeRejectWithUndefined();
     return promise.forget();
   }
 
   nsCOMPtr<nsIURI> thirdPartyURI;
   nsresult rv = NS_NewURI(getter_AddRefs(thirdPartyURI), aThirdPartyOrigin);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aRv.Throw(rv);
@@ -16873,44 +16898,40 @@ already_AddRefed<mozilla::dom::Promise> 
                                   nsContentUtils::IsInPrivateBrowsing(this),
                                   &isSameOrigin);
     if (isSameOrigin) {
       promise->MaybeResolveWithUndefined();
       return promise.forget();
     }
   }
 
-  // If the browser is not processing a user gesture, reject.
-  if (!UserActivation::IsHandlingUserInput()) {
-    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
-                                    nsLiteralCString("requestStorageAccess"),
-                                    this, nsContentUtils::eDOM_PROPERTIES,
-                                    "RequestStorageAccessUserGesture");
-    promise->MaybeRejectWithUndefined();
-    return promise.forget();
-  }
-
   // Check any additional rules that the browser has.
   if (CookieJarSettings()->GetRejectThirdPartyContexts()) {
     RefPtr<BrowsingContext> bc = GetBrowsingContext();
     if (!bc) {
+      this->ConsumeTransientUserGestureActivation();
       promise->MaybeRejectWithUndefined();
       return promise.forget();
     }
 
     nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateContentPrincipal(
         thirdPartyURI, NodePrincipal()->OriginAttributesRef());
 
     if (!principal) {
+      this->ConsumeTransientUserGestureActivation();
       promise->MaybeRejectWithUndefined();
       return promise.forget();
     }
 
     RefPtr<Document> self(this);
 
+    // Consume user activation before entering the async part of this method.
+    // This prevents usage of other transient activation-gated APIs.
+    this->ConsumeTransientUserGestureActivation();
+
     auto performFinalChecks = [inner, self, principal]() {
       RefPtr<ContentBlocking::StorageAccessFinalCheckPromise::Private> p =
           new ContentBlocking::StorageAccessFinalCheckPromise::Private(
               __func__);
       RefPtr<StorageAccessPermissionRequest> sapr =
           StorageAccessPermissionRequest::Create(
               inner, principal,
               // Allow
@@ -17020,17 +17041,20 @@ already_AddRefed<mozilla::dom::Promise> 
 
               return ContentBlocking::AllowAccessFor(
                   principal, bc,
                   ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI,
                   performFinalChecks);
             })
         ->Then(
             GetCurrentSerialEventTarget(), __func__,
-            [promise] { promise->MaybeResolveWithUndefined(); },
+            [self, promise] {
+              self->NotifyUserGestureActivation();
+              promise->MaybeResolveWithUndefined();
+            },
             [promise] { promise->MaybeRejectWithUndefined(); });
 
     return promise.forget();
   }
 
   promise->MaybeResolveWithUndefined();
   return promise.forget();
 }