Skip to content

Integrate with new draft cookie spec (draft-annevk-johannhof-httpbis-cookies/00+ε) #1807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
146 changes: 114 additions & 32 deletions fetch.bs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,28 @@ urlPrefix:https://tc39.es/ecma262/#;type:dfn;spec:ecma-262
url:realm;text:realm
url:sec-list-and-record-specification-type;text:Record
url:current-realm;text:current realm

urlPrefix:https://www.ietf.org/archive/id/draft-annevk-johannhof-httpbis-cookies-00.html#;type:dfn;spec:cookies
url:name-cookie-store-and-limits;text:cookie store
url:name-parse-and-store-a-cookie;text:parse and store a cookie
url:name-parse-a-cookie;text:parse a cookie
url:name-store-a-cookie;text:store a cookie
url:name-retrieve-cookies;text:retrieve cookies
url:name-serialize-cookies;text:serialize cookies

<!-- TODO: pending HTML changes- ancestor enum (https://github.com/whatwg/html/pull/10559), has storage access bit, initiator origin plumbing -->
urlPrefix:https://html.spec.whatwg.org#;type:dfn;spec:html
url:TODO;text:ancestry;for:environment
url:TODO;text:has storage access;for:environment
</pre>

<pre class=biblio>
{
"COOKIES": {
"authors": ["Johann Hofmann", "Anne Van Kesteren"],
"href": "https://www.ietf.org/archive/id/draft-annevk-johannhof-httpbis-cookies-00.html",
"title": "Cookies: HTTP State Management Mechanism"
},
"HTTP": {
"aliasOf": "RFC9110"
},
Expand Down Expand Up @@ -1938,6 +1956,10 @@ not always relevant and might require different behavior.
"<code>client</code>" or an <a for=/>origin</a>. Unless stated otherwise it is
"<code>client</code>".

<p>A <a for=/>request</a> has an associated
<dfn export for=request id=concept-request-navigation-initiator-origin>top-level navigation initiator origin</dfn>, which is
an <a for=/>origin</a> or null. Unless stated otherwise it is null.

<p class=note>"<code>client</code>" is changed to an <a for=/>origin</a> during
<a lt=fetch for=/>fetching</a>. It provides a convenient way for standards to not have to set
<a for=/>request</a>'s <a for=request>origin</a>.
Expand Down Expand Up @@ -2226,31 +2248,38 @@ or "<code>object</code>".
<hr>

<div algorithm>
<p>A <a for=/>request</a> <var>request</var> has a
<dfn for=request id=concept-request-tainted-origin>redirect-tainted origin</dfn> if these steps
return true:
<p>A <a for=/>request</a> has a <dfn for=request id=concept-request-redirect-taint>redirect-taint</dfn>,
which is "<code>None</code>", "<code>Cross-Origin</code>", or "<code>Cross-Site</code>".
<p>To get <a for=/>request</a> <var>request</var>'s <a>redirect-taint</a>:

<ol>
<li><p><a for=/>Assert</a>: <var>request</var>'s <a for=request>origin</a> is not
"<code>client</code>".

<li><p>Let <var>lastURL</var> be null.

<li><p>Let <var>crossOriginTaint</var> be "<code>None</code>".

<li>
<p><a for=list>For each</a> <var>url</var> of <var>request</var>'s <a for=request>URL list</a>:

<ol>
<li><p>If <var>lastURL</var> is null, then set <var>lastURL</var> to <var>url</var> and
<a for=iteration>continue</a>.

<li><p>If <var>url</var>'s <a for=url>origin</a> is not <a for=/>same site</a> with
<var>lastURL</var>'s <a for=url>origin</a> and <var>request</var>'s <a for=request>origin</a> is
not <a for=/>same site</a> with <var>lastURL</var>'s <a for=url>origin</a>, then return "<code>Cross-Site</code>".

<li><p>If <var>url</var>'s <a for=url>origin</a> is not <a>same origin</a> with
<var>lastURL</var>'s <a for=url>origin</a> and <var>request</var>'s <a for=request>origin</a> is
not <a>same origin</a> with <var>lastURL</var>'s <a for=url>origin</a>, then return true.
not <a>same origin</a> with <var>lastURL</var>'s <a for=url>origin</a>,
then let <var>crossOriginTaint</var> be "<code>Cross-Origin</code>"..
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be "same-site" or "same-site-cross-origin"?

Also, you overwrite a variable with "set" and "to". "Let ... be" is only for initializing.

Single dot at the end here too.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to redefine the enum to match Sec-Fetch-Site, omitting none. That would make this same-site, yes.


<li>Set <var>lastURL</var> to <var>url</var>.
</ol>

<li>Return false.
<li>Return <var>crossOriginTaint</var>.
</ol>
</div>

Expand All @@ -2262,8 +2291,8 @@ run these steps:
<li><p><a for=/>Assert</a>: <var>request</var>'s <a for=request>origin</a> is not
"<code>client</code>".

<li><p>If <var>request</var> has a <a for=request>redirect-tainted origin</a>, then return
"<code>null</code>".
<li><p>If <var>request</var>'s <a for=request>redirect-taint</a> is not "<code>None</code>",
then return "<code>null</code>".

<li><p>Return <var>request</var>'s <a for=request>origin</a>,
<a lt="ASCII serialization of an origin">serialized</a>.
Expand Down Expand Up @@ -2372,8 +2401,8 @@ source of security bugs. Please seek security review for features that deal with
"<a for="embedder policy value"><code>credentialless</code></a>", then return true.</p>

<li><p>If <var>request</var>'s <a for=request>origin</a> is <a>same origin</a> with
<var>request</var>'s <a for=request>current URL</a>'s <a for=url>origin</a> and <var>request</var>
does not have a <a for=request>redirect-tainted origin</a>, then return true.</p>
<var>request</var>'s <a for=request>current URL</a>'s <a for=url>origin</a> and <var>request</var>'s
<a for=request>redirect-taint</a> is not "<code>None</code>", then return true.</p>

<li><p>Return false.</p>
</ol>
Expand Down Expand Up @@ -2489,6 +2518,9 @@ this is also tracked internally using the request's <a for=request>timing allow
<p>A <a for=/>response</a> has an associated <dfn for=response>has-cross-origin-redirects</dfn>
(a boolean), which is initially false.

<p>A <a for=/>response</a> has an associated <dfn for=response>has-cross-site-redirects</dfn>
(a boolean), which is initially false.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it strikes me that we should turn these two into a "redirect taint" enum for responses?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a bit cleaner. I thought it would have the side effect of being a bit harder to read, unless we define algorithms for these two booleans which do the two obvious operations. But now that I type an example out, I like the enum even better.

e.g. compare "If response's has-cross-origin-redirects is false" to "If response's redirect taint is not 'cross-origin'".


<hr>

<p>A <dfn export id=concept-network-error>network error</dfn> is a <a for=/>response</a> whose
Expand Down Expand Up @@ -3292,6 +3324,72 @@ through TLS using ALPN. The protocol cannot be spoofed through HTTP requests in

<h2 id=http-extensions>HTTP extensions</h2>

<h3 id=cookie-header>`<code>Cookie</code>` header</h3>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a dedicated Cookies section and the the Cookie header can be one part of that.

The other part will need to tackle johannhof/draft-annevk-johannhof-httpbis-cookies#13. In particular defining requirements for user agents around clearing cookies and how those changes are propagated across documents. At least I think we want all of the architecture to be handled by Fetch and then have HTML, Cookie Store API, and Service Workers (although maybe no Service Workers changes are required? I don't recall) call into that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shuffled. That's how I had it in my first draft anyway.


<p>The `<dfn export http-header id=http-cookie><code>Cookie</code></dfn>`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's appropriate to <dfn> it. We should do this more akin to Content-Type.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

request <a for=/>header</a> allows the request to carry locally stored state, such as user credentials.

<div algorithm>
<p>To <dfn id=append-a-request-cookie-header>append a request `<code>Cookie</code>` header</dfn>,
given a <a for=/>request</a> <var>request</var>, run these steps:
<ol>
<li><p>Let |sameSite| be the result of [=determining the same-site mode=] for <var>request</var>.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is wrong here.

<li><p>Let |isSecure| be false.
<li><p>If <var>request</var>'s <a for=request>client</a> is a <a>secure context</a>, then set |isSecure| to true.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it strictly based on the URL scheme?

Copy link
Author

@bvandersloot-mozilla bvandersloot-mozilla Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested on Chrome, Safari, and Firefox. Chrome and Firefox allow write-then-read of Secure attribute having cookies on localhost.

I have no idea when this happened because I thought the same. Maybe I didn't test what I wanted?

<li><p>Let |httpOnlyAllowed| be true.
<p class=note>Fetch implies that the request is http-only, as opposed to document.cookie
<li><p>Let |cookies| be the result of running <a>retrieve cookies</a> given
|isSecure|,
<var>request</var>'s <a for=request>current URL</a>'s <a for=url>host</a>,
<var>request</var>'s <a for=request>current URL</a>'s <a for=url>path</a>,
|httpOnlyAllowed|, and
|sameSite|

<p class=note>It is expected that the cookie store returns an ordered list of cookies
<li>If |cookies| <a for="list">is empty</a>, then return.
<li>Let |value| be the result of running <a>serialize cookies</a> given |cookies|.
<li><a for="header list">Append</a> (`<code>Cookie</code>`, <var>value</var>) to <var>request</var>'s <a for=request>header list</a>.
</ol>
</div>

<div algorithm>
<p>To <dfn id=parse-and-store-response-cookie-headers>parse and store response `<code>Set-Cookie</code>` headers</dfn>,
given a <a for=/>request</a> <var>request</var> and a <a for=/>response</a> <var>response</var>, run these steps:
<ol>
<li><p>Let |allowNonHostOnlyCookieForPublicSuffix| be false.
<li><p>Let |isSecure| be false.
<li><p>If <var>request</var>'s <a for=request>client</a> is a <a>secure context</a>, then set |isSecure| to true.
<li><p>Let |httpOnlyAllowed| be true.
<p class=note>Fetch implies that the request is http-only, as opposed to document.cookie
<li><p>Let |sameSiteStrictOrLaxAllowed| be true if the result of [=determine the same-site mode=] for |request| is "<code>StrictOrLess</code>", and false otherwise.
<li><p><a for=list>For each</a> <var>header</var> of <var>response</var>'s <a for=response>header list</a>:
<ol>
<li><p>If <var>header</var>'s <a for=header>name</a> is not a <a>byte-case-insensitive</a> match for `<code>Set-Cookie</code>`, then <a for=iteration>continue</a>.
<li><p><a>Parse and store a cookie</a> given
<var>header</var>'s <a for=header>value</a>,
|isSecure|,
<var>request</var>'s <a for=request>current URL</a>'s <a for=url>host</a>,
<var>request</var>'s <a for=request>current URL</a>'s <a for=url>path</a>,
|httpOnlyAllowed|,
|allowNonHostOnlyCookieForPublicSuffix|, and
|sameSiteStrictOrLaxAllowed|
</ol>
</ol>
</div>

<div algorithm>
<p>To <dfn>determine the same-site mode</dfn> for a given <a for=/>request</a> <var>request</var>, run these steps:
<ol>
<li><p><a for=/>Assert</a>: <var>request</var>'s <a for=request>method</a> is "GET" or "POST".
<li><p>If <var>request</var>'s <a for=request>top-level navigation initiator origin</a> is not null and is not <a for=/>same site</a> to <var>request</var>'s <a for=request>URL</a>'s <a for=url>origin</a>, return "<code>UnsetOrLess</code>".
<li><p>If <var>request</var>'s <a for=request>method</a> is "GET" and
<var>request</var>'s <a for=request>destination</a> is "document", return "<code>LaxOrLess</code>".
<li><p>If <var>request</var>'s <a for=request>client</a>'s <a for=environment>ancestry</a> is "<code>cross-site</code>", return "<code>UnsetOrLess</code>".
<li><p>If <var>request</var>'s <a for=request>redirect-taint</a> is "<code>Cross-Site</code>", return "<code>UnsetOrLess</code>".
<li><p>Return "StrictOrLess".
</ol>
</div>

<h3 id=origin-header>`<code>Origin</code>` header</h3>

<p>The `<dfn export http-header id=http-origin><code>Origin</code></dfn>`
Expand Down Expand Up @@ -4680,9 +4778,12 @@ steps:
<!-- If you are ever tempted to move this around, carefully consider responses from about URLs,
blob URLs, service workers, HTTP cache, HTTP network, etc. -->

<li><p>If <var>request</var> has a <a for=request>redirect-tainted origin</a>, then set
<li><p>If <var>request</var>'s <a for=request>redirect-taint</a> is not "<code>None</code>", then set
<var>internalResponse</var>'s <a for=response>has-cross-origin-redirects</a> to true.

<li><p>If <var>request</var>'s <a for=request>redirect-taint</a> is "<code>Cross-Site</code>", then set
<var>internalResponse</var>'s <a for=response>has-cross-site-redirects</a> to true.

<li><p>If <var>request</var>'s <a for=request>timing allow failed flag</a> is unset, then set
<var>internalResponse</var>'s <a for=response>timing allow passed flag</a>.

Expand Down Expand Up @@ -5710,21 +5811,9 @@ run these steps:
<p>If <var>includeCredentials</var> is true, then:

<ol>
<li>
<p>If the user agent is not configured to block cookies for <var>httpRequest</var> (see
<a href=https://httpwg.org/specs/rfc6265.html#privacy-considerations>section 7</a> of
[[!COOKIES]]), then:
<p class=note>This permits some implementations to choose to not support cookies for some or all <var>httpRequest</var>s.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"This" here refers to the word "should" in the line below? I'm not a huge fan of that, both because it's a bit subtle and also because it doesn't account for the opposite case: All major browsers have settings / overrides that will allow cross-site cookies to be sent, so I think it would be preferable to insert a step that allows user agents to make an additional implementation defined choice about whether to include cookies or not. It might have to live in the append a request Cookie header algorithm.

Obviously this should be reserved for truly implementation-specific settings and anything else should be standardized here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"This" here refers to the word "should" in the line below?

Yes.

Yeah, we can move the user-configuration logic into append a request Cookie header.

Since you left this comment I split out the partitioning bits, so that may just resolve it.


<ol>
<li><p>Let <var>cookies</var> be the result of running the "cookie-string" algorithm (see
<a href=https://httpwg.org/specs/rfc6265.html#cookie>section 5.4</a> of
[[!COOKIES]]) with the user agent's cookie store and <var>httpRequest</var>'s
<a for=request>current URL</a>.

<li>If <var>cookies</var> is not the empty string, then <a for="header list">append</a>
(`<code>Cookie</code>`, <var>cookies</var>) to <var>httpRequest</var>'s
<a for=request>header list</a>.
</ol>
<li><p>The user agent should <a>append a request `<code>Cookie</code>` header</a> for <var>httpRequest</var>.

<li>
<p>If <var>httpRequest</var>'s <a for=request>header list</a>
Expand Down Expand Up @@ -6288,14 +6377,7 @@ optional boolean <var>forceNewConnection</var> (default false), run these steps:
<li><p>Set <var>response</var>'s <a for=response>body</a> to a new <a for=/>body</a> whose
<a for=body>stream</a> is <var>stream</var>.

<li><p tracking-vector>If <var>includeCredentials</var> is true and the user agent is not
configured to block cookies for <var>request</var> (see
<a href=https://httpwg.org/specs/rfc6265.html#privacy-considerations>section 7</a> of
[[!COOKIES]]), then run the "set-cookie-string" parsing algorithm (see
<a href=https://httpwg.org/specs/rfc6265.html#set-cookie>section 5.2</a> of [[!COOKIES]]) on the
<a for=header>value</a> of each <a for=/>header</a> whose <a for=header>name</a> is a
<a>byte-case-insensitive</a> match for `<code>Set-Cookie</code>` in <var>response</var>'s
<a for=response>header list</a>, if any, and <var>request</var>'s <a for=request>current URL</a>.
<li><p tracking-vector>If <var>includeCredentials</var> is true, the user agent should <a>parse and store response `<code>Set-Cookie</code>` headers</a> given <var>request</var> and <var>response</var>.

<li>
<p>Run these steps <a>in parallel</a>:
Expand Down
Loading