Skip to content

Add usage advice for Sec- #1818

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 4 commits into
base: main
Choose a base branch
from

Conversation

martinthomson
Copy link
Contributor

@martinthomson martinthomson commented Mar 31, 2025

There are a lot of examples where the Sec- prefix is used without a lot of consideration for why.

This change is an attempt to articulate why you might want to use this prefix and deny a website the ability to set a value for a header.

Examples of Sec- that make the platform worse include Sec-CH- prefixed headers, which all engage server content negotiation capabilities that sites might be able to use. I would include Sec-Browsing-Topics in this category also, but maybe for more reasons than one.

Examples that are mostly just pointless include Sec-GPC and Sec-Purpose, which both have no security-relevant decision that might be made by a server.

The Sec-Fetch-Dest and Sec-Fetch-Mode headers are good examples of things that would have security consequences if they weren't prefixed. We also have a bunch that are forbidden and not prefixed that make a bunch of sense, like Connection.

I make an argument for Sec-WebSocket-Key in the text, which seems pretty solid to me. I can't make a similar argument for the other websocket headers. Sec-WebSocket-Accept is a response-only header, so the prefix makes no sense other than for naming consistency, which is a bad reason.

Part of the reason for this is that we're seeing a bunch of cargo-culting in the definition of headers. Take device-bound session credentials (https://github.com/w3c/webappsec-dbsc), which defines a response header called Sec-Session-Registration. There, the reason appears to be consistency with request header naming, but it's not clear that the request headers themselves need a Sec- prefix either.


Preview | Diff

There are a lot of examples where the `Sec-` prefix is used without a lot of consideration for why.

This change is an attempt to articulate why you might want to use this prefix and deny a website the ability to set a value for a header.

Examples of `Sec-` that make the platform worse include `Sec-CH-` prefixed headers, which all engage server content negotiation capabilities that sites might be able to use.  I would include `Sec-Browsing-Topics` in this category also, but maybe for more reasons than one.

Examples that are mostly just pointless include `Sec-GPC` and `Sec-Purpose`, which both have no security-relevant decision that might be made by a server.

The `Sec-Fetch-Dest` and `Sec-Fetch-Mode` headers are good examples of things that would have security consequences if they weren't prefixed.  We also have a bunch that are forbidden and not prefixed that make a bunch of sense, like `Connection`.

I make an argument for `Sec-WebSocket-Key` in the text, which seems pretty solid to me. I can't make a similar argument for the other websocket headers.  `Sec-WebSocket-Accept` is a response-only header, so the prefix makes no sense other than for naming consistency, which is a bad reason.

Part of the reason for this is that we're seeing a bunch of cargo-culting in the definition of headers.  Take device-bound session credentials (https://github.com/w3c/webappsec-dbsc), which defines a response header called `Sec-Session-Registration`.  There, the reason appears to be consistency with request header naming, but it's not clear that the request headers themselves need a `Sec-` prefix either.
@domenic
Copy link
Member

domenic commented Mar 31, 2025

I am interested in your example

The [Sec-Purpose](https://whatpr.org/fetch/1818.html#http-sec-purpose) field tells a server that a request is speculative. A server might choose to avoid triggering side-effects while processing such a request, such as suppressing the recording of page view metrics. Making this a forbidden request-header has no security-relevant purpose and the Sec- prefix is therefore unnecessary.

(To double-check, this is not an example of the advice directly above it about CORS preflights, right? It's just coincidentally right after that, as something else you don't agree with.)

I think this falls into a general category where servers are better served by getting accurate information about the purpose of a request, but indeed getting inaccurate information isn't a security problem. (At least, not a security problem more serious than a DOS.)

In such cases, I've advised spec writers that using Sec- is a reasonable default. That is, if there's no compelling use case for letting fetch() fake a fetch-involving web platform feature, and servers benefit from mostly getting accurate information about such fetches, then we should default to using Sec- to help servers out.

A recent example is our design of Sec-Speculation-Tags, where we say

We've proposed using the Sec- prefix since we do not believe there are use cases for allowing web developers to manipulate these headers from JavaScript via a service worker, or set them with fetch() calls. Allowing such manipulation would not necessarily break anything, but it is probably simpler for web developers if they can always trust that a Sec-Speculation-Tags header comes from an actual speculation rules-initiated request.

There's also somewhat of a self-reinforcing argument here, because it could be confusing for servers to receive a request with Speculation-Tags but not Sec-Purpose. That is, since Sec-Purpose uses Sec-, even if we believe that was not necessary, it's probably better to have other features that build on Sec-Purpose also use Sec-.

@martinthomson
Copy link
Contributor Author

martinthomson commented Mar 31, 2025

I thought that that would attract comment :)

To double-check, this is not an example of the advice directly above it about CORS preflights, right? It's just coincidentially right after that [...]

Yeah, I struggled with the transition there. It's coincidental positioning only.

I have heard a number of people who say that they received advice about the Sec- prefix of the form that you describe. And there's a definite pattern of cargo-culting or at least naming consistency being used to justify more of it. That does real harm with things like Sec-UA-etc... where apps can't choose to hook into the capabilities that the header might otherwise enable.

Your example of speculation is one where the harm isn't obvious. Why would an app want to trigger a fetch marked as prefetch? If you can't imagine a reason, there's a tendency to slap a Sec- on and move on. But that is just a failure of imagination. We just can't imagine a case where someone might choose to fetch that way. I can easily imagine a site wanting to prefetch something on the basis of JS code deciding that a particular navigation is imminent. Why not also for speculative fetches as well? I can well imagine that the number of cases where the user agent is in a better position to drive that, but why deny apps that option?

(Of course, the need for a preflight could make the prefetch too slow to be useful, but that's a separate problem.)

@johannhof
Copy link
Member

Martin, thanks for writing this up - Another reason that we liked using Sec- for Storage-Access Headers was that the Sec- namespace is automatically reserved by virtue of the entire prefix being forbidden, making conflicts with existing custom application headers impossible.

Do you have any thoughts on that and should we consider this in advice we give to browser developers?

@martinthomson
Copy link
Contributor Author

I don't know what you mean by reserved here.

If you mean reserved for this purpose (and not some other purpose), that is why we have IANA registries. The storage access headers appear to be registered, so that's probably not it... (That's not true for a few other headers that others have started to use. Including Sec-Fetch-*, which is not great...calling @mikewest.)

Reserving a header for the exclusive use of a user agent is somewhat appealing as a user agent developer. You say "mine" and that's the end of the story. No further thought. But adopting that position as a default denies sites the option to use the header.

That's probably OK for Sec-WebSocket-Key, which is really only for browsers to use anyway. Sec-CH-* doesn't meet that bar, Sec-Purpose doesn't, and nor does the Sec-Fetch-Storage-Access (which is a really, really long header name to be sending on EVERY request, by the way). If a site believes that it has storage access and it doesn't or vice versa, then it might not work properly, but that's on the fool that sets the header to the wrong value. There's no security-relevant decision riding on it being correct; those decisions are made based on the content of cookies.

@johannhof
Copy link
Member

I mean it's impossible to have prior usage of this header on the web, which avoids the pain of finding out whether you're going to break someone's site with it.

@martinthomson
Copy link
Contributor Author

My understanding is that - for new headers like this - HTTP archive is the benchmark. Are you talking about saving one query to that database?

martinthomson and others added 2 commits April 1, 2025 20:04
Co-authored-by: Mike West <mike@mikewest.org>
Co-authored-by: Mike West <mike@mikewest.org>
Co-authored-by: Simon Pieters <zcorpan@gmail.com>
@johannhof
Copy link
Member

My understanding is that - for new headers like this - HTTP archive is the benchmark. Are you talking about saving one query to that database?

HTTP Archive covers public traffic, i.e. non-logged in scenarios and non-enterprise. This is a great suggestion but I'm not sure it's sufficient, it's certainly not the same as "literally guaranteed to have no prior usage on the Web". Given that we have a simple mechanism that can make that guarantee for both browsers and servers, why not use it?

@mnot
Copy link
Member

mnot commented Apr 3, 2025

Is avoiding breaking even a single site really a reasonable goal here, considering that a site can also change its private use if there is a clash? Many new headers have been introduced without a Sec- prefix over the years and haven't caused significant breakage.

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

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

If we are going to redefine what it means to extend the same-origin policy we should not do that here.

requests</a> include [:Access-Control-Request-Method:], which is [=forbidden
request-headers|forbidden=]. Any <a for=/>headers</a> that a fetch caller sets will not be set on a
<a>CORS-preflight request</a> made by an honest user agent; instead, these are listed in
[:Access-Control-Request-Headers:].
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this is a compelling example as these were introduced prior to the introduction of the prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I disagree. It's quite a compelling example in that it is forbidden for a very good reason. (I don't know why you quoted the unrelated text about ACRH, is there a point you wanted to make there?

<a for=/>request</a> is speculative. A server might choose to avoid triggering side-effects while
processing such a request, such as suppressing the recording of page view metrics. Making this a
<a>forbidden request-header</a> has no security-relevant purpose and the `<code>Sec-</code>` prefix
is therefore unnecessary.
Copy link
Member

Choose a reason for hiding this comment

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

This is a redefinition of the threat model. See w3c/resource-hints#74 (comment) for a discussion of this header.

Sending new headers across origin without preflight and without a Sec- prefix is an extension of the same-origin policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I traced the origin of Sec-Purpose to that thread. You do not establish a reason for it being forbidden, but instead assume that to be the case. If there was a risk that a server might be confused by the presence of the field, that would have been obvious. That case was not made, only asserted without evidence.

Sending new headers has to be safe in HTTP. We routinely add new headers in the IETF and have not encountered significant problems from having done so. Obviously, there is always a risk of collision, but we have things like the HTTP archive to aid in identifying potential conflicts. The point about preflight is only relevant if there is a) a conflict, b) a security-relevant decision being made that relies on the values being correct, and c) a resource that has harmful side effects on requests that are not already subject to preflight. That's a pretty tight set of conditions.

I can make a clear case for Sec-Fetch-Dest as it pertains to JSONP (perhaps) and maybe some forms of script inclusion.. That clearly justifies the use of a prefix.

@johannhof
Copy link
Member

I agree with Anne's point about sending new headers across origins being an SOP issue and I like that this formulates a clear objective rule to follow here.

I think it also makes it more clear to me what made me uneasy about this PR - it tries to impose a subjective decision upon browser developers to take their best guess on whether or not their header could be security relevant - with a bias towards not adding the Sec- prefix, i.e. the less secure path, which seems like the inverse of how these kinds of security-related decisions should be made.

@martinthomson it would be good if you could clearly formulate your reasons for wanting to stop people from "cargo-culting" on Sec-. Is it about the extra bytes? If that is a concern, we should have a fundamental and data-informed discussion about the impact of larger header names and how we could define clear rules to keep them smaller, such as deprecating Sec- in favor of adding new headers to the forbidden request header list. As I pointed out, I believe there is real value in having a separate header namespace reserved for user agents, but I'm not married to that idea if there is value in abandoning it.

From a security standpoint, there should be clear rules though, and I support the threat model that Anne puts forward here.

@martinthomson
Copy link
Contributor Author

@annevk I find your claim about SOP confusing. This has very little to do with SOP or the security model. I'm not looking to change the definition of Sec-: there are clear examples of where it is valuable. I'm just seeking to establish clear rationale by which someone might choose to apply it that don't involve reasoning of the form "just in case".

@johannhof The pull request makes the case pretty clearly already. Maybe you disagree. The problem is that denying script the option to request content with forbidden fields forces the use of alternative spellings of the exact same semantics. Consider Sec-CH-DPR. Is there any security reason why a site should not be permitted to request content with a higher DPR than the screen on which the browser assumes it might be displayed?

I don't know that a threat model has been documented, but I don't think that the one you both seem to have settled on is useful. If we consider preflight to be effective in blunting the effectiveness of attacks, and that preflight is included whenever a non-safelisted field is added to a request (at least when using the API; by the way, there is a reason that people always end up asking @annevk to interpret this specification, I had a really hard time working this out; I'm still not confident that I've 100% covered that). There are then no cases where an "attacker"-controlled request appears without first getting permission from the resource.

More fundamentally, if new fields cannot be sent to servers, we have a major problem. New fields are defined all the time. There can be good reasons for browsers to send them. They will appear in form submissions. There's a good reason that the list of forbidden fields includes old fields, because those are more likely to be acted upon by servers that are not aware of CORS. But for new fields defined so long after CORS became ubiquitous, it's silly to insist that a server might act on it in a way that can be exploited.

The threat model I documented is - at least in my view - a clearer one. That is, if a server needs to make a security-relevant decision with confidence that the value of the Sec--prefixed field came from the same place that the credentials did, then it is a good use of a forbidden header.

@johannhof
Copy link
Member

The threat model I documented is - at least in my view - a clearer one. That is, if a server needs to make a security-relevant decision with confidence that the value of the Sec--prefixed field came from the same place that the credentials did, then it is a good use of a forbidden header.

@martinthomson I don't believe that it is possible to objectively and completely define "needs" and "security-relevant" in this sentence. One origin should just not be able to manipulate state that another origin expects to receive from the user agent, like permission state. I think I agree that there are cases where we can decide to make an explicit exception to this principle, maybe DPR is one of those, but that's the way we should approach this question, not the other way around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

7 participants