w3c / webappsec-permissions-policy

A mechanism to selectively enable and disable browser features and APIs
https://w3c.github.io/webappsec-permissions-policy/
Other
399 stars 155 forks source link

Permissions Policy JS API #401

Open clelland opened 4 years ago

clelland commented 4 years ago

Permissions Policy currently has the JavaScript API that was specced when it was Feature Policy, but it turns out that the semantics are now a bit different, because of the way that the header is interpreted and combined with the container policy. (#357, #378)

The policy.allowsFeature(feature, origin) method currently returns whether origin is in policy's allowlist for feature (or is part of the default allowlist).

With the old header behaviour, this answered the question "would this feature be allowed in a document from that origin, in an iframe with no allow attribute?" -- that is, would the feature be automatically delegated to that origin.

Now, with the new behaviour, being present in that allowlist does not imply that the feature would be automatically delegated. Instead, for a third-party origin, it means that the feature could be delegated, if the allow attribute is used. (This gets even more vague and tentative if we start looking at an iframe element's policy object, because then it tests whether the feature could be delegated, by another iframe tag inside the framed document, if the framed document matches the src attribute, and hasn't been navigated to another origin, and if the framed document's header policy doesn't change anything)

We could resolve this in a few different ways:

  1. Do nothing, and inform developers of the change in the meaning of the results
  2. Rewrite the algorithms to return the answer to the original question (but this answer is almost always no; at least for features with a default allowlist of 'self')
  3. Remove the origin parameter from the method, and only test the policy's own origin
  4. Remove the method entirely.

Number 3 is probably possible; I've been looking for any evidence of usage of that API on the web, and in the entirety of HTTPArchive and the top 100k sites in the Chrome User Experience report, there is absolutely none. Sites I can observe in the wild only use document.featurePolicy.allowsFeature(feature) -- no origin, and not on an iframe element, just the document. The only usage I can find anywhere of the other forms of the API are in WPT, and those can be removed / rewritten.

annevk commented 4 years ago

I would prefer 4 unless there is a clear way this API can help in a way that the more widely deployed permissions.query cannot.

clelland commented 3 years ago

The uses that I can see are of two forms:

  1. WPT uses allowsFeature(feature) extensively as a general-purpose mechanism for testing whether features are correctly delegated.

    • Those could be rewritten to use some other feature detection mechanism, but we'd need to ensure that all of them have such a mechanism, or else forego testing their permissions-policy integration.
    • Not all have integrated with the Permissions API, as far as I know. (Should permissions.query be updated to support features like fullscreen, sync-xhr, idle detection, usb, webauthn, xr-spatial-tracking, etc?)
  2. I've seen uses that test whether accelerometer and gyroscope are allowed, before registering an orientationChange event handler.

    • Those could be updated to use permissions.query.
    • Chrome at least would need to deprecate the API over some time period, to give sites a chance to update.
annevk commented 3 years ago

sync-xhr is not a Permissions Policy thingy, but for the others, yes, I think so. The more we can converge Permissions and Permissions Policy, the better.

cc @johannhof

johannhof commented 3 years ago

I agree and I can imagine updating permissions.query for that.

clelland commented 3 years ago

Luckily, synchronous XHR has easily-detectable failure modes, so doesn't necessarily need support from permissions.query, but it certainly is a thing, since https://github.com/whatwg/xhr/pull/177. (Permissions Policy is a rename from Feature Policy; not a completely new thing).

Support for the sync-xhr feature is implemented in Blink and WebKit (WPT), and it's one of the most used features on the web: Chrome metrics sees it used in an allow attribute in something like 0.75% of all page views. We certainly need to consider it when making changes.

craigfrancis commented 3 years ago

Could the JS API provide the same document.featurePolicy.features() and document.featurePolicy.allowedFeatures()?

I use these to check that my FeaturePolicy header is specifying something for everything (as a default would be risky); and while I could check the permissions_policy_features.json5 file, that doesen't show which ones are currently enabled.


var sent = JSON.parse(meta_ref.getAttribute('content')), // Header keys, JSON encoded, in a <meta> tag.
    accepted = document.featurePolicy.features(),
    skipped = sent.filter(function(a) { return !accepted.includes(a) });

var ignore = ['cross-origin-isolated'],
    allowed = document.featurePolicy.allowedFeatures().filter(function(a) { return !ignore.includes(a) });
clelland commented 3 years ago

Yeah, I think that usage shows that we need something like that still. I have something of a concrete proposal that I'm writing up; I'll post it here so folks can see whether it covers the right use cases.

clelland commented 3 years ago

Based on Chrome's UMA data (added in Chrome 91, which has been in stable release for a week or so, but I've been watching it since it was the Canary channel), there are only three components of the Feature Policy JS API which are used in practice:

The remaining API surface is effectively unused.

The feature list is by far the most used API, at a startling 11% of page visits. allowsFeature and allowedFeatures lag far behind at ~0.05% and ~.0005% respectively.

Given the rename of Feature Policy to Permissions Policy, the low usage of most of the API, and what seems like an apparent similarity between Permissions Policy and the Permissions API, there seems to be justification for merging the APIs. In practice, this would mean folding the functionality of the Permissions Policy JS API into the Permissions API. I have some concrete suggestions for doing that here:

1. Replace uses of document.featurePolicy.allowsFeature(feature) with navigator.permissions.query(feature)

This isn't an exact replacement; there are a couple of a couple of discrepancies that will need to be resolved:

To fix the first, we'd need to expand the permissions registry to include these new non-permission features. This currently has support, but may raise the bar in the future for adding new features.

For the second, we need to look at the practical difference between permissions.query and featurePolicy.allowsFeature.

https://w3c.github.io/permissions/#reading-current-states already integrates with permissions policy, to return 'denied' if it is denied by policy.

There are essentially 6 states that the feature can be in: (2x3) Allowed by policy, denied by policy; allowed by user, denied by user, user never asked (prompt).

  Policy: Denied Policy: Allowed
User: Denied Denied Denied
User: Prompt Denied Prompt
User: Allowed Denied Allowed

Currently the policy side is revealed by the featurePolicy interface, while the user side is exposed by permissions.query. Merging the two APIs would mean that there is no way to distinguish between "denied by policy" and "denied by user".

Q: Are there UI cases where "denied by policy" should be treated differently than "allowed by policy; denied by user"? I think the only thing it would allow is to give websites an extra opportunity to tell the user that they could change their mind, but this seems contradictory to the user's stated intent, and I don't know if that's a pattern that should be encouraged.

For most practical purposes, permissions.query would be a usable replacement for allowsFeature.

2. Move the feature list to navigator.permissions.features.

This is a harder move, as document.featurePolicy.features has non-trivial amounts of usage. Again, we would need to include other permissions policy features in the Permissions enum. I do think it makes sense in the long term, though. Once usage in the wild moves to permissions.features, I expect that the entire document.featurePolicy object can be deprecated.

3. Remove document.featurePolicy.allowedFeatures

This API could be replaced with specific calls to query the state of individual features, rather than providing a list. Especially if the list of available features can be iterated over, it would be trivial to get this with the other API calls.

4. Remove document.featurePolicy.allowsFeature(feature, origin), featurePolicy.getAllowlistForFeature, and all of HTMLIframeElement.featurePolicy

These APIs see essentially zero usage in the wild. Their primary function seems to be to support WPT, by allowing the state of the policy in the browser to be probed. These tests can mostly be replaced with behavioral tests, and if there are tests that absolutely require access to the interior state of the policy, then a TestDriver interface can be added to expose that.

craigfrancis commented 3 years ago

With the "11% of page visits", I assume that's some browser fingerprinting going on? I have a slight worry, even though I'm using it, that it's being used for less than ideal purposes?

clelland commented 3 years ago

I don't know -- it's a possibility, but document.featurePolicy.features() essentially maps 1:1 to browser version.

My suspicion (mostly because I've seen examples) is that there are a couple of widespread libraries in the wild doing something like

if (document.featurePolicy.features().includes('featureX'))
  some_frame_element.allow = "featureX";

which also isn't necessary at all, but I've seen it done.

clelland commented 3 years ago

It's also possible that it's being used as a generic feature-detection mechanism, rather than for any sort of permission control or delegation.

craigfrancis commented 3 years ago

That's interesting (and strange they do that), thanks for checking... otherwise, while I don't have a vote, I'd be happy with any of those suggestions.

annevk commented 3 years ago

I think I would prefer to not expose .features unless there is a compelling use case.

craigfrancis commented 3 years ago

My use case for .features() is to keep an eye on which features I can add to the permissions-policy header (my homepage listing my projects/tools checks this API every day).

I want to keep as many restrictions in place (I'm hosting sensitive medical data), but I don't see a default working because some things shouldn't be disabled by default (e.g. cross-origin-isolated).

It's not as though the folder of policies is kept up to date.

And I can't check permissions_policy_features.json5 as that's for Chrome Canary, and has no indication which depends_on flags are enabled by default - e.g. ch-dpr is enabled, but ambient-light-sensor is not.

annevk commented 3 years ago

That seems like a transitional problem, but will over time be solved through improved documentation.

craigfrancis commented 3 years ago

So we will rely on all things being documented, and website developers checking the list every month or so?

Considering document.featurePolicy.features() already exists, why can't we effectively keep it? while I was thinking about the fingerprinting risk, as Ian points out it "essentially maps 1:1 to browser version".

fergald commented 2 years ago

FYI, I have posted https://github.com/w3c/webappsec-permissions-policy/issues/444 proposing a Permissions-Policy: unload header.

eeeps commented 2 years ago

@clelland:

Remove document.featurePolicy.allowsFeature(feature, origin) [and] featurePolicy.getAllowlistForFeature […] These tests can mostly be replaced with behavioral tests

Is there any way to test for Client Hint features that doesn't involve an actual request across the network? That's expensive.