Skip to content
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

Add new Access-Control-Suppress-Headers CORS response header #253

Closed
roryhewitt opened this issue Mar 18, 2016 · 18 comments
Closed

Add new Access-Control-Suppress-Headers CORS response header #253

roryhewitt opened this issue Mar 18, 2016 · 18 comments

Comments

@roryhewitt
Copy link

As an alternative/supplement to the existing Access-Control-Expose-Headers CORS response header, could we have a new header, as defined below:

Access-Control-Suppress-Headers = "Access-Control-Suppress-Headers" ":" #field-name

Access-Control-Suppress-Headers and Access-Control-Expose-Headers are mutually exclusive. If both headers are returned in response to a CORS preflight OPTIONS request, all non-simple headers are suppressed (not exposed).

Otherwise, if Access-Control-Suppress-Headers is returned (in response to a CORS preflight OPTIONS request), the list of exposed headers must be updated to include all simple response headers and any headers other than those of which the field name is an ASCII case-insensitive match for one of the values of the Access-Control-Suppress-Headers headers (if any), before exposing response headers to APIs defined in CORS API specifications.

Therefore, even if the Access-Control-Suppress-Headers includes a simple header, that header will still be exposed - the Access-Control-Suppress-Headers value applies only to non-simple headers.

Essentially, where Access-Control-Expose-Headers whitelists response headers (disallow all except those specified), Access-Control-Suppress-Headers blacklists response headers (allow all except those specified).

Why do I want this?

As often as not, when developers use the Access-Control-Expose-Headers, what they really want is to ensure that certain headers are not available to client-side code - effectively, they have to list all the allowed headers. If the backend code changes and new headers are added, the code which sets the Access-Control-Expose-Headers value must also be updated. By allowing those developers to explicitly define which headers should be hidden from client-side code, it gives them the ability to not worry about having to explicitly add new headers to the Access-Control-Expose-Headers 'list'.

Where CDN's and load balancers are brought into the mix, this also makes it easier for them, since this is another place where the CORS preflight OPTIONS request (including the Access-Control-Expose-Headers response header) may be handled, so simplifying here would be useful.

(Full disclosure: I work for Akamai, a large CDN. My views do not represent any official Akamai position. That being said, implementing this might make our (and out customer's) lives easier. I have also implemented CORS solutions using an F5 LTM load balancer, and run into this problem).

@annevk
Copy link
Member

annevk commented Mar 18, 2016

@sicking @tyoshino?

I could see this as a complimentary header to AC-Expose-Headers that takes precedence in case of conflict. I'm not sure we should make them mutually exclusive. That way you can have a server-wide policy of suppressing certain debug headers, while individual resources can expose what they think is relevant and you don't have to worry about individual resources leaking too much (unless hey include the information elsewhere, but then you're already done for).

@sicking
Copy link

sicking commented Mar 18, 2016

Can you provide an example of when you wouldn't want a certain header to be exposed? But you still want to send that header to the client?

I.e. if there are certain headers that you know you don't want the client to see, why send them at all?

@roryhewitt
Copy link
Author

Hey Jonas,

That's a good question!

I don't have a good example, because I don't think there is one. This is
why I'm frustrated by the current AC-Expose-Headers - it was (I think) the
wrong solution.

In short, AC-Expose-headers is a whitelisting mechanism - it defines which
of the sent headers should be exposed to client-side code. Well in what
cases would there be a header which you do want to send, but which you
don't want to expose? Presumably there are such cases, because
AC-Expose-Headers was created based on feedback from mnot (who works with
me at Akamai!). Prior to AC-Expose-Headers being created, presumably all
response headers were exposed, and there wa sa concern that perhaps some of
them should not be. well in that case, perhaps a better mechanism would be
AC-Suppress-headers, where users can blacklist certain headers, leaving all
others exposed.

At least, that's my thinking.

@sicking
Copy link

sicking commented Mar 18, 2016

Before Access-Control-Expose-Headers was added no headers except "safe" headers (cache-control, content-language, content-type, expires, last-modified and pragma) were exposed.

Without an actual use case I don't think we should attempt to resolve this issue as filed.

@sicking
Copy link

sicking commented Mar 18, 2016

Here is where the feature was added to Gecko: https://bugzilla.mozilla.org/show_bug.cgi?id=597301#c13

@annevk
Copy link
Member

annevk commented Mar 20, 2016

Indeed, see https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name for the safelist (the names listed in expose are an modification to that safelist).

@roryhewitt
Copy link
Author

@sicking, so prior to AC-Expose-Headers, no headers were exposed except for the ones on the safelist? And then when AC-Expose-Headers was added, we had the ability to specify additional specific headers to expose?

I agree with the original idea that not exposing any non-safe headers is too restrictive. The problem as I see it, is that without allowing either of the following:

Access-Control-Expose-Headers: *

or

Access-Control-Suppress-Headers: <list-of-headers>

then we are still hobbled by the fact that developers need to update their CORS setup every time they add a new header to an API response which might need to be accessed by client-side code. I know this doesn't seem like a big deal, but in my experience working with development teams, there can be a huge disconnect between the back-end developers, front-end developers and whoever is in charge of the web server.

I think that Access-Control-Suppress-Headers would be a simpler, and safer solution - allowing a team to specify the following:

Access-Control-Suppress-Headers: X-Secure, X-Debugging

would be a one-time change that would allow development teams to add new response headers on an ongoing basis, without needing to update the Access-Control-Expose-Headers value each time. It's also clearer to both ends what is being done - specific headers are being suppressed.

This would change the spec to be as follows (addition of the line at the end):


A CORS-safelisted response-header name, given a header list list, is a header name that is one of:

Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma.
Any value resulting from parsing Access-Control-Expose-Headers in list that is not a forbidden response-header name.
The above list must exclude any value resulting from parsing Access-Control-Suppress-Headers in list.


Originally I said that I thought that Access-Control-Expose-Headers and Access-Control-Suppress-Headers should be mutually exclusive, but on further consideration, it makes sense if Access-Control-Suppress-Headers overrides Access-Control-Expose-Headers. In other words, if the response includes the following:

Access-Control-Expose-Headers: X-Header-1, X-Header-2, X-Header-3
Access-Control-Suppress-Headers: X-Header-2

then the CORS-safelisted response-header names for the request would be:

Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma.
X-Header-1
X-Header-3

So there would be no error if both Access-Control-Suppress-Headers and Access-Control-Expose-Headers are specified, although typically only one of them would be specified.

@sicking
Copy link

sicking commented Mar 22, 2016

I agree that Access-Control-Expose-Headers: * would be useful. And probably safe enough that we should add it.

You still have not provided a use case for Access-Control-Suppress-Headers. I.e. when would a developer knowingly want to send a header to the client, but not expose it to the webpage?

The whole design philosophy behind CORS is that server-side developers should not be required to have perfect understanding of all the code that is running on your webserver but still allow them to expose data that they want to expose to 3rd parties.

So even on a server where you might be handling sensitive user data, or where you might have scripts that do sensitive transactions on the server, you can white-list certain URLs and let those send and receive data. And you can do that without having to perfectly audit the rest of the URL-handling scripts on the server.

But say that the server-side developer know that some URLs serves some data that should be shared with 3rd parties, and some data that should not be shared with 3rd parties. In that case, simply stop sharing the data that should not be shared with third parties before you opt in to CORS for that URL.

It seems silly to at that point opt in to CORS but knowingly keep sending the data that should not be shared, and send a header saying "I know i'm sending this data, but please don't share it with the website". Simply stop sending the data instead.

Or, to put it another way. If you can't perfectly audit what data you are sending in which headers, use Access-Control-Expose-Headers: a, b, c to opt in to sharing the headers that you know are safe. If you can perfectly audit what data you are sending in which headers, then stop sending any data that should not be shared, and then use Access-Control-Expose-Headers: *

@roryhewitt
Copy link
Author

@sicking, OK, here's a use-case for Access-Control-Suppress-Headers. You
may argue it's a little contrived :)

A developer has multiple endpoints (API's) on the same server. Each API
returns multiple headers, some of which are returned by all APIs, and some
of which are API-specific. The developer doesn't necessarily have direct
access to setting the CORS headers (maybe that's done in the web server
config or in a load-balancer or by a CDN). Or maybe they do, but they're
looking for simplicity.

So we have something like this:

getCustomerData() returns x-header1, x-secure and x-header2
getCustomerStatus() returns x-header1 and x-header3
setCustomerStatus() returns x-header1, x-secure, x-header2 and x-header3
In this case, all API's return x-header1, and the others are only
returned by some API's. The x-secure header is only returned by two of
the API's, and it should only be exposed if the request comes from the same
domain as the API.

If we only allow Access-Control-Expose-Headers, then the generation of that
header line must be done by the developer (or by someone with explicit
knowledge about which headers are allowed to be returned by which API and
which are not). So the developer would have to generate the following
header lines which are specific to each API:

getCustomerData() - Access-Control-Expose-Headers: x-header1, x-header2 (plus x-secure if request coming from *.example.com
getCustomerStatus() - Access-Control-Expose-Headers: x-header1, x-header3
setCustomerStatus() - Access-Control-Expose-Headers: x-header1, x-header2, x-header3 (plus x-secure if request coming from *.example.com

However, if we are able to simply specify this:

Access-Control-Suppress-Headers: x-secure (unless request coming from
*.example.com

then this can be done in the load-balancer or CDN, by someone with only
minimal knowledge of the underlying application. All they need to know is
that x-secure MAY be returned by the application, and if so, it should not
be exposed unless the request came from *.example.com.

Additionally, if a new header, x-header4 is being added to any of the
above API's, then the various Access-Control-Expose-Headers lines need to
be changed in the code to add it. Whereas if they have used
Access-Control-Suppress-Headers:
x-secure
, then there is no additional work - x-header4 will
automatically be exposed.

Essentially, if we make security simpler to implement, then more people
will use it correctly. This gives them the option to suppress things which
they don't want exposed, with minimal fuss.

I think the problem here is that server-side developers are not a single
group - there are often multiple different groups involved (especially in
large organizations, and doubly-so where those organizations are not
themselves tech companies). Giving an organization the ability to simplify
their setup, whether it's in their Apache configuration or somewhere else
which is not controlled by the actual server-side developer is a good
thing, no?

Note: I have updated my disclosure at the bottom of the original post.

@roryhewitt
Copy link
Author

@sicking, in fact, the two header options lend themselves well to working together. The following code could be used (as before, implemented in many possible places 'up the chain' between the backend code and the browser - generic init code, Apache config, load-balancer, CDN, whatever):

Access-Control-Expose-Headers: *
Access-Control-Suppress-Headers: x-secure

Browsers would treat this to mean that all headers (except for x-secure) should be exposed. So we get the benefit of the generic "expose all headers" with the specific "except for this one header, if it's passed". No subsequent code changes needed to expose new headers (unless, of course, the new header should not be exposed).

@sicking
Copy link

sicking commented Mar 22, 2016

Yes, I think the use-case is contrived. Doesn't feel common enough to add specific features for. It's even more contrived when you consider the fact that it's only useful for requests that include credentials which is a minority use case in and of itself.

It's also supported through multiple other means:

  • Use separate URI spaces for same-origin and cross-origin clients.
  • Check the origin header and don't output the sensitive data for cross-origin requests.
  • Use some form of authentication tokens and only include the sensitive data when that token is present. Make sure to include the token for same-origin requests.

And yes, I understand that * and the Suppress header can work together. But so far the only use cases for doing so are very contrived and can be solved through multiple other means.

@roryhewitt
Copy link
Author

@sicking, yes, I thought you'd say that :)

I don't want to beat a dead horse here, but:

With all due respect to yourself, Anne, Craig (and any others who have been involved in the 3 issues I have raised recently as well as the development of the CORS standard), I am concerned that you have been lucky enough to have had experiences (within the 'pure tech' world of browser development) that are more 'limited' than mine.

My experience is the opposite - I think it's actually pretty common to have the sort of setup that I described - in fact, it's taken from real life. I'm saying this from many years working with customers in the retail, manufacturing and financial segments, as well as developing software myself. Obviously my experiences may not mirror the wider 'web community' and I hesitate to extrapolate too far, but I'm explicitly not coming from the pure tech side. I' have seen different teams develop different areas of the application(s), which are then updated over the years by other teams. Documentation is minimal (if existent at all). Most people understand their own language/team/tool (and may be excellent at it!), but may not have much understanding of the wider scope of things. For instance, they can put together a great Java application to retrieve data from the backend and build a JSON response, but they don't know about more than the basics, since some other software will actually deal with sending that data to the browser.

Additionally, my experience (again!) is that most requests are made with credentials - basically, to pass/retrieve cookies. For instance, retail sites (amongst many others) use lots of cookies, and often need to include them with every request. If in doubt, developers will set xhr.withCredentials = true; and then go from there (and spend a long time trying to figure out why an ACAO value of * doesn't work).

I agree however, that there are multiple ways round this. I just thought that this was simpler for the end user to understand and implement.

Finally, why do you say that this is this only an issue for requests that include credentials?

@sicking
Copy link

sicking commented Mar 23, 2016

For requests without credentials, any header can easily be read by simply using a non-browser HTTP client.

I.e. I can use wget to make any URL from any server, using any method and any headers that I want. And I can read the full response including all response headers.

The only thing I can't do, is make that request with your cookies.

@annevk
Copy link
Member

annevk commented Mar 23, 2016

@sicking, the agreement for Access-Control-Expose-Headers: * does not extend to credentialed requests? I guess that makes sense.

It still seems sensible to have something like Access-Control-Exclude-Headers (easier to spell) to me as a crude tool for the developer guarding the network boundary. Without such a tool they'd need to manually filter responses which seems a lot more error prone.

@sicking
Copy link

sicking commented Mar 23, 2016

In this comment I said I'm fine with adding Access-Control-Expose-Headers: *. I don't really have a strong opinion about if we add it only for credential-less requests, or for all requests.

I don't think we should add Access-Control-Exclude-Headers. It's a convenience feature to support a use case which seems rare to me, and which is already supported through multiple other means as listed here.

All of these other ways seems more security-wise sound and have the advantage that they work on existing browsers.

@roryhewitt
Copy link
Author

@sicking, maybe I'm confused, but surely I can use cURL to make a cross-domain GET request passing/receiving cookies (also passing the requisite CORS headers and responding appropriately to the preflight OPTIONS request/response)? In that case, wouldn't I be able to see any GET response headers, regardless of what value might be returned in Access-Control-Expose-Headers?

At any rate, I'd prefer to allow Access-Control-Expose-Headers: * on both credentialed and non-credentialed requests. I think if we only allow it for credentialed requests, it will be too restrictive.

@annevk
Copy link
Member

annevk commented Mar 25, 2016

@roryhewitt you can't do that with someone else their cookies. I recommend studying https://annevankesteren.nl/2015/02/same-origin-policy.

I agree with @sicking that a big concern with exclude would be that it's not backwards compatible.

Therefore, it's probably best not to add this. #252 exists for expanding expose, so closing this.

@dveditz
Copy link
Member

dveditz commented May 5, 2016

By allowing those developers to explicitly define which headers should be hidden from client-side code, it gives them the ability to not worry about having to explicitly add new headers to the Access-Control-Expose-Headers 'list'.

Conversely, it allows those developers to footgun themselves by adding a new sensitive header and forgetting to update the AC-Suppress-H list. If your application needs the data you'll quickly figure it out if you forget to update ACEH; forgetting to update ACSH leads to a security vulnerability. A long and painful history teaches us that forgetting is likely either way. We strongly lean towards causing work for developers versus trouble for everyone.

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

No branches or pull requests

4 participants