privacycg / storage-access

The Storage Access API
https://privacycg.github.io/storage-access/
199 stars 27 forks source link

Make implicit deny and explicit deny indistinguishable #60

Open johnwilander opened 3 years ago

johnwilander commented 3 years ago

Implicit deny == the browser decides this third party is not allowed to request storage access and immediately rejects. This could be the result of policy or a user facing feature à la "Don't ask me again." Explicit deny == the user gets prompted and chooses "Don't allow."

WebKit/Safari has seen misuse of the Storage Access API where the caller measures the time for the document.requestStorageAccess() promise to resolve/reject and changes behavior based on whether it was implicit or explicit. The goal seems to be to pressure users to allow storage access if they get prompted. In the specific case, the tap to play a video both starts playback and calls document.requestStorageAccess(). If the user is prompted and explicitly denies storage access, the video stops. The user can clearly see that it's possible to watch the video without storage access but is punished for not opting in. We've received multiple reports of this.

This is a tricky issue because of timing. One way is to always delay the resolve/reject. Another is to hang rather than reject and only execute the promise completion handler on resolve. A third would be to offer the user to some way lie, along the lines of "tell them I said yes but actually block access." I'm not sure the third option is something we could explain to users.

othermaciej commented 3 years ago

Out of these, I don't think the third button is a good approach, because it's confusing to have two ways to say no. I could see the case for denial always working that way, but I think the embed could detect that cookies are blocked, so could create the same problem. So that leaves delay or never reject as the only options. It's easier to be confident that never-reject can't be gamed. But embeds where the user is not logged in at all might want to pop up an oauth window, and would not be able to decide whether to do that in a never-reject world. On the third-hand, they also wouldn't know when to go to an OAuth flow in a delayed-reject world. So maybe never-reject is the best solution.

johnwilander commented 3 years ago

Is there precedence for hanging a promise by spec? The user could wait virtually forever to dismiss the prompt so it could already happen in practice but I'm thinking it might be a new thing to specify such behavior.

We should discuss what such a change would mean for the preserved user gesture on implicit reject. Maybe we'd have to preserve the user gesture for some period of time to allow the caller to do a popup or we do what we considered very early on and let the caller specify a popup to be shown on implicit reject and thus make that the only allowed behavior in that case. I believe we considered supporting a subset of the parameters for window.open() and also restricting it to the same registrable domain as the calling origin in the iframe.

othermaciej commented 3 years ago

This probably needs to be discussed in a call. I think it's worthwhile to have a goal of "can't distinguish user saying no vs user having no state". Another option (maybe WebKit-specific) is to apply never-reject or delayed-reject only to known abusers (as determined by the UA).

jkarlin commented 3 years ago

A fourth option is to resolve the deny case with ephemeral? storage. At which point, the third party knows that the user either rejected, or it's a new user, and proceeds accordingly. I don't love the idea of not telling the third party what state it's in, but I'm not sure it leads to drastically different results for the third party. But what if the third-party re-requests storage access, because now the user really does want to give it and it tells the third party as much. Will it prompt again or auto-resolve? If it prompts again, the same timing attack shows that the user previously rejected. so it must resolve. Which means that once denied, the user can't really get out of that situation.

johnwilander commented 3 years ago

The recovery path problem is different in my view. It already applies to shipping behavior. Safari allows two prompts per requesting iframe before auto rejecting. I believe Firefox and Edge have converged on one prompt per top frame load in which case a page reload is the recovery path. I think we said on a recent Privacy CG call that we'll consider moving to that model too.

jackfrankland commented 3 years ago

A fourth option is to resolve the deny case with ephemeral? storage.

In this scenario the third party will not know if they have unpartitioned storage access or not, which doesn't sound great to me. Presumably they would lose access to partitioned storage, which they may prefer over the new ephemeral storage, and it would mean making sure other methods of detecting access would need to be indistinguishable too (i.e. Set-Cookie response header).

We should discuss what such a change would mean for the preserved user gesture on implicit reject.

I think in any case the preserved user gesture should be looked at, because it is also a distinguishing factor between implicit and explicit rejects that exists now. Even with a delayed reject (that doesn't consume the user gesture), you could test the difference with the success or failure of a user activation gated api.

Is there precedence for hanging a promise by spec?

An issue I can see with never rejecting is keeping the call stack in memory unnecessarily and not being able to garbage collect any references.

If there is a subsequent user gesture on the hierarchy of the top-level document, does that mean that the dialogue has been either granted/denied/ignored? With a collaborating first party, the third party could infer that an implicit or explicit reject has taken place anyway - so perhaps rejecting (for both implicit and explicit) at the time of a subsequent user gesture could be an option?

brodrigu commented 3 years ago

Implementing a "feature" to solve for this seems like a slippery slope. The APIs are working as intended here. The user gets a degraded experience because the developer in your example seems to be offering content in exchange for storage access (presumably for advertising purposes) but not being upfront about it and teasing the user with the video then taking it away when they decline.

It will always be possible for a site to annoy a user in some way but the user has an option to not visit these sites. However, if the browser begins misleading websites with artificially delayed promises or inaccurate data it will cripple the tools developers use to build great user experiences and result in a degraded user experience across the entire web.

annevk commented 3 years ago

I'm not sure I fully follow @jkarlin's alternative. If this is a site that doesn't appear on any lists it will have partitioned access, right? So the main question is whether it will get non-partitioned access?

Ideally a site cannot distinguish between a user that rejects now and a user that has a setting set to always reject. It seems a site can tell whether a user has rejected in the past if the partitioned storage is still around. So a random delay for prior decisions could be done, but is perhaps slightly weird if the site has access to that anyway (if it cared to store it).

If the browser rejects due to a site appearing on a list a random delay might not be needed as that will be consistent across users.

annevk commented 3 years ago

I think the F2F on this concluded with John wanting to try only ever resolving the promise (if the user accepts or has accepted in the past) and otherwise leave it alone as unresolved/unrejected.

But the question that raised for equivalent patterns (e.g., Notification.requestPermission()) went unaddressed.

hober commented 2 years ago

@johnwilander do you know the current state of this issue? What's the state of each implementation on this? Would a delay help, or is 'never resolving' the right way to go?

bvandersloot-mozilla commented 1 year ago

Following up that I think this is a good issue and any solution to #37 should consider this.

One alternative to never rejecting is to always reject an identical exception after a fixed, finite time. Either that or never rejecting in the event of a deny are equally suitable to me.

annevk commented 1 year ago

The current state here seems to be that #37 excluded this from its scope, but @bvandersloot-mozilla worked on a new PR specifically for this issue: #120.

annevk commented 1 year ago

@jyasskin if we used the Permissions API infrastructure for Storage Access as discussed in #121, is it possible to use https://w3c.github.io/permissions/#dfn-permission-state-constraints to return "prompt" when it wants to return "denied"? Otherwise we could not hide "implicit deny" (as per OP) from the web developer.

(It's probably okay if lack of Permissions Policy or secure contexts does result in "denied". At least those are not currently subject to constraints it seems and I don't think they need to be as they are static enough.)

jyasskin commented 1 year ago

@annevk I think you might need to modify the Permissions API to make this work, but there are good reasons to let all permissions disguise whether they were denied if the UA thinks that's what the user wants. The "permission state constraints" probably aren't the right way to do it: IIRC I meant them for the mostly-unused ability to include other data with the query() response, like a list of devices that have been granted.

annevk commented 1 year ago

Thanks, I filed https://github.com/w3c/permissions/issues/388 to track that.

KOLANICH commented 1 year ago

But what if the third-party re-requests storage access, because now the user really does want to give it and it tells the third party as much. Will it prompt again or auto-resolve? If it prompts again, the same timing attack shows that the user previously rejected. so it must resolve. Which means that once denied, the user can't really get out of that situation.

IDK how it is done in Chromium, but Firefox has an icon, clicking to which a user can edit the permissions he has given to the website (only the ones a website has requested are shown there). So a user can change his mind without triggering a promise, and the next request for the permissions should be resolved. What is lacking I guess is some non-intrusive indication when a website requests a permission. I guess it can be animating that icon with the iconnof the permission being requested and its state.

remko commented 9 months ago

Educational tools like ours are typically integrated in Learning Management Systems using iframes (e.g. Google Classroom Add-ons, Microsoft Teams, LTI-supporting systems, …). These integrations depend on storage from within the iframe to complete sign-in procedures and maintain sessions, so we have to use the Storage Access API in e.g. Safari to make this work.

Users often use the LMS integration as the main entry point to our service. It’s therefore not unusual that users who use our service every day through the LMS never have had any first-party interaction with our site in the past.

We found that the immediate failure of document.requestStorageAccess() is almost always due to the lack of first-party interaction, so we use the timing based approach described above to detect immediate failure, and offer a message to the user with a procedure to trigger first-party interaction.

I’m worried that if the failures become indistinguishable, our already vague messaging to the user on what they could try to remedy the problem will become even harder.

lghall commented 7 months ago

Chiming in from the Google Docs team. We have built out an interstitial page that is shown when an embedded Google Doc requires storage access to authenticate. This page has a button prompting the user to click to grant storage access to the iframe.

If storage access is rejected, we need to tell the user what to do. In this case, if the access was explicitly denied, we want to show them an error page saying they cannot view the content unless they grant cookie access to google.com on this page. However, if the access was implicitly denied, we should tell them to log-in in a 1p context and then try again.

Right now we can't disambiguate between these 2 cases, which makes designing the user experience quite challenging. It would be helpful if there was a way to disambiguate between why the storage access was rejected.

johannhof commented 7 months ago

Thanks for the feedback! I would agree that revealing the lack of "prior user interaction" (to be specified in #190) might be an acceptable trade-off from a privacy and anti-abuse perspective as it significantly improves the user experience in the non-abuse case.

I understand the goal of this issue to avoid sites punishing or pressuring the user based on their prompt responses, but it feels like this is still achieved when making that specific carve-out. We'd still not reveal other browser-based rejections, e.g. preventing repeated prompts to fight prompt spam.

This would obviously not work in combination with "never resolving the promise".

@annevk @bvandersloot-mozilla thoughts?

bvandersloot-mozilla commented 7 months ago

I think this carve out makes sense. And user interaction is already web-observable. +1 from me.