fetch icon indicating copy to clipboard operation
fetch copied to clipboard

Discussion: the term for non-preflighted request ("simple request")

Open elarlang opened this issue 7 months ago • 9 comments

What is the issue with the Fetch Standard?

Problem to solve: What is "the official" term for an HTTP request that does not trigger a CORS preflight?

Previously, it was called a "simple request". https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF#avoiding_simple_requests

Till there is a better name, the usage of the "simple request" moves on.

I have used and recommended the term "CORS-safelisted request", but in the specification document, it is not clearly stated, although the following terms are used:

  • CORS-safelisted method
  • CORS-safelisted request-header
  • CORS-safelisted response-header name

But not "CORS-safelisted request" as an independent term.

It is a "close call" in https://fetch.spec.whatwg.org/#cors-protocol-exceptions

Specifications have allowed limited exceptions to the CORS safelist for non-safelisted Content-Type header values. These exceptions are made for requests that can be triggered by web content but whose headers and bodies can be only minimally controlled by the web content. Therefore, servers should expect cross-origin web content to be allowed to trigger non-preflighted requests with the following non-safelisted Content-Type header values:

And then defined as "non-preflighted request".

It would be nice if the specification defines such term.

At the moment, my main question is - is the "CORS-safelisted request" somehow incorrect to use?

elarlang avatar May 15 '25 12:05 elarlang

I think it's fine to use and it's definitely much better than "simple request", but you should be aware that a CORS-preflight request can nevertheless be forced. E.g., in XMLHttpRequest if you use upload event listeners or with fetch() if you use an upload stream.

As long as we point out that caveat I'd be supportive of adding this to the specification and encouraging MDN and such to update their documentation.

cc @jub0bs @sideshowbarker

annevk avatar May 16 '25 07:05 annevk

Thanks for the ping, @annevk.

I too find "simple request" to be a problematic term, simply because it's painfully generic; practitioners may not realise that the term carries a deeper meaning than meets the eye. (Incidentally, I have similar grievances about the term "site".) I'd be in favour of establishing a more precise term in the Fetch standard.

The term "non-preflighted request", if it's to be understood as

a request initiated and composed in such a way as not to give rise to CORS preflight

strikes a good balance between conciseness and precision. The term "simple request" is likely to endure regardless, though.

jub0bs avatar May 16 '25 07:05 jub0bs

I would be happy to see the term “non-preflighted request” elevated further in the spec itself — and happy to see that the MDN docs get updated to use that.

As far as the “CORS-safelisted request”, it seems me that could be potentially confusing to developers — in that at least some might mistakenly assume that any request that just uses a CORS-safelisted method will necessarily be a “CORS-safelisted request”.

So instead using “non-preflighted request” more widely/consistently seems better because it’s precise and unambiguous.

sideshowbarker avatar May 16 '25 08:05 sideshowbarker

It does seem weird to point out that a non-preflighted request can nevertheless be preflighted, but I guess that works.

annevk avatar May 16 '25 08:05 annevk

@annevk

It does seem weird to point out that a non-preflighted request can nevertheless be preflighted, but I guess that works.

It would indeed be unfortunate if "non-preflighted" came to mean "sometimes preflighted". But I'm not sure I'm following you. Are you referring to your earlier comment?

a CORS-preflight request can nevertheless be forced. E.g., in XMLHttpRequest if you use upload event listeners or with fetch() if you use an upload stream.

FWIW, the term "simple request", as used by MDN Web Docs, covers those cases:

  • If the request is made using an XMLHttpRequest object, no event listeners are registered on the object returned by the XMLHttpRequest.upload property used in the request; that is, given an XMLHttpRequest instance xhr, no code has called xhr.upload.addEventListener() to add an event listener to monitor the upload.
  • No ReadableStream object is used in the request.

Can you clarify what you're worried about?

jub0bs avatar May 16 '25 08:05 jub0bs

Thank you for the responses and information about the caveats for using the term "CORS-safelisted".

Incidentally, I have similar grievances about the term "site"

First, a bit off-topic, but +100 for this and the context where I try to clarify the term "CORS-safelisted" is a security requirement to mitigate against so-called cross-site request forgery attack (fyi, which I prefer to name browser-side request forgery).

The proposed "non-preflighted request" is well understandable and precise, if the context for the term is already CORS.

If the context is not CORS by default, like it is in my case or as it is in the referenced MDN article, only using "non-preflighted request" may raise questions.

Maybe "non-CORS-preflighted request" does the job for this?

elarlang avatar May 16 '25 09:05 elarlang

Should the two opposing terms used to describe the request type span both same-origin and cross-origin scenarios, or be explicitly only bothered with cross-origin scenarios? This changes what the two terms can be that would still be logically coherent.

Example argument for "should span both same-origin and cross-origin scenarios":

  1. the Fetch Standard itself spans both scenarios;
  2. it may be easier to precisely talk about certain situations, starting first from the action -- request and its type ("simple" / "non-simple") -- and then delve into the context -- relations of the request initiator and the request destination (like same-origin / cross-origin, same-site / cross-site)

To illustrate the second point, here's a recent example where I had to describe when are cookies attached to requests. This IMO gets more convoluted (more depth and possible alternatives) when NOT starting first from the request type:

Example

I. Starting first from request type ("simple" / "non-simple"):

Cookies are attached to ...

a. "simple" request
a.1. same-site => always
a.2. cross-site => depends on browser settings, SameSite attribute etc.

b. "non-simple" request
b.1. same-origin => yes by default (if not credentials: omit)
b.2. cross-origin => no by default (preflighted + SameSite restrictions still apply; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)

^ note that using "non-preflighted" and "preflighted" instead of "simple" and "non-simple" wouldn't hold up here, since "preflighted" would be a parent of "same-origin", which doesn't make sense (same-origin requests are not preflighted)

II.a) Starting first from initiator-destination relation (origin):

Cookies are attached to ...

a. same-origin request => yes by default (if not credentials: omit)

b. cross-origin request
b.1. same-site 
b.1.1. "simple" => always
b.1.2. "non-simple" => no by default (preflighted; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)
b.2. cross-site
b.1.1. "simple" => depends on browser settings, SameSite attribute etc. 
b.1.2. "non-simple" => no by default (preflighted + SameSite restrictions still apply; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)

or alternatively (I think best alternative)

a. same-origin request => yes by default (if not credentials: omit)

b. cross-origin request
b.1. "simple"
b.1.1. same-site => always
b.1.2. cross-site => depends on browser settings, SameSite attribute etc.
b.2. "non-simple" => no by default (preflighted  + SameSite restrictions still apply; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)

II.b) Starting first from initiator-destination relation (site):

Cookies are attached to ...

a. same-site request
a.1. same-origin => yes by default (if not credentials: omit)
a.2. cross-origin
a.2.1. "simple" => always
a.2.2. "non-simple" => no by default (preflighted; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)
b. cross-site requests
b.1. "simple" => depends on browser settings, SameSite attribute etc.
b.2. "non-simple" => no by default (preflighted  + SameSite restrictions still apply; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)

or alternatively

Cookies are attached to ...

a. same-site request
a.1. "simple" => always
a.2. "non-simple"
a.2.1. same-origin => yes by default (if not credentials: omit)
a.2.2. cross-origin => no by default (preflighted; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)

b. cross-site request
b.1. "simple" => depends on browser settings, SameSite attribute etc.
b.2. "non-simple" => no by default (preflighted + SameSite restrictions still apply; preflight needs to minimally return Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: <origin>)
Assumptions
  1. Same-origin requests are never preflighted. (Even if we wanted to trigger a CORS-preflight request manually, by definition, the request requires Access-Control-Request-Method which is a forbidden request header)
  2. Cross-origin requests can be preflighted: a. they are not preflighted if the request is "simple" (safe methods, safe headers, no event listeners are registered on the object returned by XMLHttpRequest.upload, etc.) b. they are preflighted otherwise ("non-simple" type of request)

A table illustrating this possible viewpoint:

"simple" / "CORS-safe" "non-simple" / "CORS-unsafe"
same-origin request never preflighted never preflighted
cross-origin request never preflighted always preflighted

So we can say:

same-origin requests are never preflighted, whether they are "simple" or "non-simple"; cross-origin requests are preflighted only if they are "non-simple"

This mental model breaks down if instead of "simple" / "non-simple" ("CORS-safe" / "CORS-unsafe", or anything similar), we use the term pair "non-preflighted" & "preflighted" to mean the same thing, as the table above doesn't hold up anymore:

"non-preflighted" "preflighted"
same-origin request never preflighted never preflighted (contradiction!)
cross-origin request never preflighted always preflighted

Contradiction: a same-origin request is never preflighted, but is now categorized under "preflighted" since it is a "non-simple" request. What should we then call a "non-simple" same-origin request if we have made "non-simple" mean "preflighted"?


If we believe the two opposing terms should explicitly be only bothered with cross-origin scenarios, then "non-preflighted" and "preflighted" are totally fine, as the table becomes just a single row (doesn't even need to be a table):

"non-preflighted" "preflighted"
cross-origin request never preflighted always preflighted

For same-origin requests, then, there's no "type" to talk about really. We can only talk about just "same-origin requests" in general, not specifying anything further. This means we have to always specify initiator-destination relation first (same-site? same-origin?), then talk about the request type only if it's cross-origin.

ukusormus avatar May 16 '25 12:05 ukusormus

@ukusormus To summarise your comment (correct me if I'm wrong):

  • The term simple request, as it commonly used (e.g. by MDN Web Docs), refers only to a non-preflighted request (i.e. a request that does not give rise to CORS preflight) that is also cross-origin.
  • All same-origin requests are non-preflighted, but not all non-preflighted requests are same-origin.

I think the term non-preflighted request, if it becomes enshrined in the Fetch standard, should also encompass same-origin requests; otherwise, as you pointed out, there would be room for ambiguity. If necessary, when using the term "non-preflighted request", the standard would need to specify whether it's referring to a cross-origin one.


Incidentally, the following passage isn't quite right:

Cookies are attached to ...

a. "simple" request
a.1. same-site => always
a.2. cross-site => depends on browser settings, SameSite attribute etc.

A simple request may or may not carry credentials, whether it be same-origin, cross-origin but same-site, or cross-site. With the Fetch API, it all depends on the value of the credentials property.

jub0bs avatar May 17 '25 10:05 jub0bs

@jub0bs Yes, seems like a valid summary (but I'm not sure about the conclusion). If we extend the request "type" (e.g. non-preflighted/preflighted) to any request, we can have a (mental or technical) pipeline where we first divide an incoming request into two buckets "non-preflighted" and "preflighted", and later decide (e.g., when getting the request initiator information from somewhere), is it same-origin or cross-origin. Although from this viewpoint, I would use some other terms than "non-preflighted" and "preflighted", since the "preflighted" bucket could then turn out to contain a "same origin" request initiator, which would make the request actually non-preflighted.

As said, this is one possible interpretation and the other (current?) interpretation could prevail that doesn't extend the request "type" to same-origin requests.

For an example counter-argument, one could say that:

  • Keep it simple: there's same-origin requests with no subtypes, and for cross-origin requests & CORS, we have these two exclusive subtypes X and Y.
  • You don't really need that reverse (mental or technical) pipeline anywhere

Could there be any reason to distinguish same-origin requests by being "simple" or "non-simple", for any reason, now or in the future?


OT:

A simple request may or may not carry credentials

True, thanks for pointing out. The "always" wording was taken from an attacker's / pentester's perspective that tries to maximize any given situation for own benefit :-) It should be more along the lines of "cookies can always be included with a simple request in same-site scope if the attacker wants it to be", be it with <form>, the Fetch API or whatnot.

(was taken from this tool I've recently put together, changed the wording slightly there)

ukusormus avatar May 17 '25 11:05 ukusormus