Skip to content

Commit

Permalink
Add latest and latest-from-network tokens
Browse files Browse the repository at this point in the history
#67 points out that it can be useful to have values that require less maintenance from the site author. This adds the latest token for the allowed=() list, and the latest-from-network token for the preferred= entry, to allow such use cases.

In the process, this expands the README's illustration of the Origin-Policy header to contain more concrete examples, and removes the spec-sketch from version-negotiation.md in favor of a more broad outline.

This also clarifies that you do not need to duplicate the preferred value inside allowed.

Closes #67. Closes #61.
  • Loading branch information
domenic committed Feb 12, 2020
1 parent f7683b3 commit a3c9cb5
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 30 deletions.
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,44 @@ For more on the policy format, see [the dedicated document](./policy-format.md).

### Fetching and applying the origin policy

Browsers then fetch and cache origin policies for a given origin. They can optionally do so proactively (e.g. for frequently-visited origins), but generally will be driven by the web application sending a HTTP response header requesting that a given origin policy be fetched and applied:
Browsers then fetch and cache origin policies for a given origin. They can optionally do so proactively (e.g. for frequently-visited origins), but generally will be driven by the web application sending a HTTP response header requesting that a given origin policy be fetched and applied.

For more on the model for fetching and updating origin policies, including motivations behind the design, see [the dedicated document](./version-negotiation.md). Here we summarize the most common patterns applications will probably use:

#### Optional-but-suggested policy

```
Origin-Policy: preferred="my-policy", allowed=("my-old-policy" null)
```

This allows a previous revision of the policy (identified by `"my-old-policy"`), or no policy at all (`null`), but specifies that the `"my-policy"` revision is preferred. This response might be processed with the old or null origin policy if those are in the HTTP cache, but in that case, the browser will perform an asynchronous update to fetch the latest policy for use with future responses.

If a _different_ origin policy is found in the HTTP cache, apart from `"my-policy"` or `"my-old-policy"`, then the response will use the null origin policy, since that is allowed.

#### Latest available policy, if any

```
Origin-Policy: preferred=latest-from-network, allowed=(latest null)
```

This says that any cached origin policy from `/.well-known/origin-policy` can be used, but if no such policy is cached, then the null policy will be used instead. In either case, the latest policy will be fetched asynchronously.

This is essentially a simplification of the previous version, where the server operator is expressing fewer constraints on the exact contents of the policy.


#### Mandatory policy

```
Origin-Policy: allowed=(null "my-policy" "my-old-policy"), preferred="my-policy"
Origin-Policy: allowed=("my-policy")
```

Here the header specifies allowed and preferred policies, which are matched against the JSON document's `"ids"` values. This allows servers to take on a variety of behaviors, including:
This says that the only origin policy that is allowed is one identified by `"my-policy"`. The origin policy must be fetched, from the cache or network, and must have an `"ids"` value that contains `"my-policy"`, before the response can be processed. If such an origin policy cannot be found, then the response will be treated as a network error.

* Require that a given origin policy be available (either from the cache or via fetching) and applied, before proceeding with page initialization
* Allow a previous revision of the policy, or no policy at all, to apply, but in the background do an asynchronous update of the policy so that future resource fetches will apply the preferred one.
This makes the most sense if the origin policy contains security-critical policies which would not be acceptable to continue without.

For more on the model for fetching and updating origin policies, see [the dedicated document](./version-negotiation.md).
### Policy expiry

Another important note is that the policy items in question automatically stop applying (in a policy item-specific way) when the origin policy stops applying. So, for example, removing the `"content_security"` member of the origin policy manifest above would cause any future loads that use that origin policy to not include the CSP in question. Combined with the usual HTTP cache expiry mechanisms for the `/.well-known/origin-policy` resource, this allows a general "max age" mechanism for origin-wide configuration, similar to the `max-age` parameter of [HSTS](https://tools.ietf.org/html/rfc6797), but for all policy items.
Policy items in question automatically stop applying (in a policy item-specific way) when the origin policy stops applying. So, for example, removing the `"content_security"` member of the origin policy manifest above would cause any future loads that use that origin policy to not include the CSP in question. Combined with the usual HTTP cache expiry mechanisms for the `/.well-known/origin-policy` resource, this allows a general "max age" mechanism for origin-wide configuration, similar to the `max-age` parameter of [HSTS](https://tools.ietf.org/html/rfc6797), but for all policy items.

### Configurable policy items

Expand Down
42 changes: 36 additions & 6 deletions index.src.html
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ <h3 id="examples">Examples</h3>
Then, change the `<a http-header><code>Origin-Policy</code></a>` response header to indicate that this new policy is preferred:

<pre>
<a http-header>Origin-Policy</a>: allowed=("policy-1" "policy-2"), preferred="policy-2"
<a http-header>Origin-Policy</a>: preferred="policy-2", allowed=("policy-1")
</pre>

When the browser sees this header value on any response from <code>https://example.com</code>, one of three things will happen:
Expand Down Expand Up @@ -260,14 +260,42 @@ <h3 id="examples">Examples</h3>
The general rule here is that when adding a new value to the "<code><a for="origin policy manifest">ids</a></code>" field of their origin policy manifest, server operators will be best served by keeping any previous [=origin policy/IDs=] as well, at least until any cached resources that reference that origin policy could have expired.
</div>

<div class="example" id="example-null">
MegaCorp has noticed that first-time visitors to their site, as in <a href="#example-initial-policy">the example above</a>, are having a suboptimal experience, with their responses being blocked by an additional round-trip to fetch the origin policy. Upon reflection, MegaCorp realizes that first-time visitors probably don't need origin policies applied. All of their site's exciting stuff is hidden behind a login wall, which involves at least one navigation. So it would be fine to omit the origin policy for such initial responses, as long as it gets applied on post-login responses.

To express this, MegaCorp modifies their response header to

<pre>
<a http-header>Origin-Policy</a>: preferred="policy-2", allowed=("policy-1" <mark>null</mark>)
</pre>

This indicates that the [=null origin policy=] is acceptable, if both "<code>policy-1</code>" and "<code>policy-2</code>" are not cached, and thus allows responses to initial visitors to proceed without blocking on fetching an origin policy. But, because there is a <code>preferred</code> value, in such cases the browser will also do a non-blocking fetch to <code>https://example.com/.well-known/origin-policy</code>, to update the HTTP cache for any future requests to <code>https://example.com/</code>.

MegaCorp could even vary their `<a http-header><code>Origin-Policy</code></a>` header dynamically between this fast-but-permissive version and the previous blocking-and-strict version, depending on characteristics of the request such as [=credentials=].
</div>

<div class="example" id="example-latest">
MegaCorp is downsizing, and can no longer spare headcount for a dedicated Origin Policy Specialist to carefully maintain their origin policy manifest's "<code><a for="origin policy manifest">ids</a></code>" field, and their `<a http-header><code>Origin-Policy</code></a>` header <code>allowed</code> and <code>preferred</code> values. They just want to perform updates to their origin policy manifest, and have them rolled out to their visitors as soon as is possible.

To make this work, they change all their response headers to

<pre>
<a http-header>Origin-Policy</a>: preferred=latest-from-network, allowed=(latest null)
</pre>

which will ensure that browser uses the latest origin policy available from the cache, if any, or the [=null origin policy=], if nothing is cached. Furthermore, the <code>preferred=latest-from-network</code> part of the header ensures that the browser will always perform a non-blocking fetch to <code>https://example.com/.well-known/origin-policy</code> to update the HTTP cache.

This is slightly less efficient than the previous version. And, it doesn't allow expressing constraints on the contents of policies, by matching preferred and allowed values with the manifest's "<code><a for="origin policy manifest">ids</a></code>" field. But, it is simpler to maintain.
</div>

<div class="example" id="example-killswitch">
MegaCorp wishes to remove the origin policy, and ensure that any visitors to its site no longer get any of its effects. It can do so by using the following `<a http-header><code>Origin-Policy</code></a>` response header:

<pre>
<a http-header>Origin-Policy</a>: allowed=(null)
</pre>

When the browser recieves this header, it will ensure that no origin policy is applied for the response, since only the [=null policy=] is allowed. It will also evict any cache entries for <code>https://example.com/.well-known/origin-policy</code>.
When the browser receives this header, it will ensure that no origin policy is applied for the response, since only the [=null policy=] is allowed.
</div>

</div>
Expand All @@ -280,14 +308,16 @@ <h3 id="origin-policy-header">The `<code>Origin-Policy</code>` HTTP header</h3>

`<a http-header><code>Origin-Policy</code></a>` is a [=structured header/dictionary=] structured header. The dictionary has two keys: [[!STRUCTURED-HEADERS]]

* <code>allowed</code>, whose value is an [=structured header/inner list=] that contains [=valid origin policy IDs=] as strings, or the token <code>null</code>, and
* <code>preferred</code>, whose value is a [=valid origin policy ID=].
* <code>allowed</code>, whose value is an [=structured header/inner list=] that contains [=valid origin policy IDs=] as strings, or the token <code>null</code>, or the token <code>latest</code>; and
* <code>preferred</code>, whose value is either a [=valid origin policy ID=], or the token <code>latest-from-network</code>.

At least one of these two entries needs to be provided for the header to have useful behavior.

<p class="note">If an ID is provided for <code>preferred</code>, it is unnecessary to also include it in the <code>allowed</code> list.</p>

No [=structured header/parameters=] are used in any locations within the structured header.

The processing model for this header, including the fallback behavior for when it is not provided or does not conform to the above data model, is given in [[#from-response]]. In general, the fallback behavior for malformed headers is to behave the same as if `<code>Origin-Policy: allowed=(null)</code>` is sent, and thus use the [=null policy|null origin policy=] for processing the [=response=]. The processing model for omitting the header is to use any previously-cached [=/origin policy=] for the [=/origin=].
The processing model for this header, including the fallback behavior for when it is not provided or does not conform to the above data model, is given in [[#from-response]]. In general, the fallback behavior for malformed headers is to behave the same as if `<code>Origin-Policy: allowed=(null)</code>` is sent, and thus use the [=null origin policy=] for processing the [=response=]. The processing model for omitting the header is to use any previously-cached [=/origin policy=] for the [=/origin=].

<h3 id="manifest-file">The origin policy manifest</h3>

Expand Down Expand Up @@ -399,7 +429,7 @@ <h2 id="data-model">Data model</h2>
: <dfn for="origin policy">content security policies</dfn>
:: A [=list=] of [=content security policy object|content security policies=]. [[!CSP]]

The <dfn>null policy</dfn> is an [=/origin policy=] whose [=origin policy/IDs=] is a list containing the single item null, and who has empty lists for its [=origin policy/feature policy=] and [=origin policy/content security policies=]. As enforced by the processing model, no other policy can have a null in its [=origin policy/IDs=] list.
The <dfn lt="null policy|null origin policy">null policy</dfn> is an [=/origin policy=] whose [=origin policy/IDs=] is a list containing the single item null, and who has empty lists for its [=origin policy/feature policy=] and [=origin policy/content security policies=]. As enforced by the processing model, no other policy can have a null in its [=origin policy/IDs=] list.

A [=/string=] is a <dfn>valid origin policy ID</dfn> if all of the following are true:

Expand Down
26 changes: 9 additions & 17 deletions version-negotiation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,15 @@ There must be a way to express that a new origin policy should apply even to tak

Conceptually, origin policies are stored in the HTTP cache, under the URL `$origin/.well-known/origin-policy`. In particular, this means they are double-keyed [in the same way as the HTTP cache](https://github.com/whatwg/fetch/issues/904): that is, by (origin of document, origin of top-document). This prevents them from being used for cross-site tracking. This also means that clearing of the cache should generally clear any origin policy.

When the browser makes a request to _url_, it:

* Checks if it has something cached, and not expired, for `$that_origin/.well-known/origin-policy`. If so, this is the _candidate origin policy_; otherwise the _candidate origin policy_ is the null policy.
* Makes the request to _url_.
* The response can contain a header of the form `Origin-Policy: allowed=("policyA" "polB" "polC" null), preferred="policyZ"`. This indicates that policies with identifiers `"policyA"`, `"polB"`, `"polC"`, or `"policyZ"` are acceptable to the site, or the null origin policy. Call these the _list of acceptable policies_. If the response contains no such header, then the _list of acceptable policies_ contains just the null policy.
* Checks the _list of acceptable policies_ against the _candidate origin policy_.
* If the _candidate origin policy_ has an ID that is in the _list of acceptable origin policies_, then:
* Apply the _candidate origin policy_ and load the response. (This might apply the null policy.)
* If _candidate origin policy_ does not contain an ID that matches the preferred origin policy (indicated by the `preferrered=` portion), then the browser sends a low-priority request to `$that_origin/.well-known/origin-policy` to refresh the HTTP cache, but it won't apply for this page load.
* Otherwise, if the _list of acceptable origin policies_ only contains the null policy but _candidate origin policy_ is not the null policy, then:
* Apply the null policy anyway, and load the response with it.
* In the background, re-fetch `$that_origin/.well-known/origin-policy` to refresh the HTTP cache.
* Otherwise, the browser makes a request (on the same connection, if HTTP/2), to `$that_origin/.well-known/origin-policy`. It delays any processing of the response for _url_ until the new policy has been loaded. If the new policy's identifier still doesn't match the _list of acceptable policies_, then the result of the origin navigation request is a network error.

Here, the IDs for an origin policy are found inside its JSON document, e.g. `"ids": ["policyA", "polB"]`.

Note the distinction between string-based policy identifiers, surrounded by quotes (e.g. `"policyA"`), and the `null` token, which is not quoted. This distinction is given to us by the structured headers specification.
A policy can have several identifiers, found in its JSON document as, for example, `"ids": ["policyA", "polB"]`.

The `Origin-Policy` header can express that it allows, or prefers, specifically-identified policies. The header also can express that it allows any policy, with the token `lastest`, or no policy at all, with the token `null`. Finally, the header can express that it prefers the latest origin policy from the network, with the token `latest-from-network`.

* ID-based matching is used when the web application wants to express constraints on the contents of the policy.
* `latest` and `latest-from-network` are used when the web application wants to ensure there is an origin policy, but does not want to take on the maintenance burden of expressing ID-based constraints.
* `null` is used when the web application is OK with a given response being processed without an origin policy, for example to avoid an extra server round-trip.

(Note the distinction between string-based policy identifiers, surrounded by quotes, and the special tokens, which are not quoted. This distinction is given to us by the structured headers specification.)

### Evaluation

Expand Down

0 comments on commit a3c9cb5

Please sign in to comment.