First of all, I assume a backend that control inputs to prevent XSS vulnerabilities.
In this answer #Les Hazlewood explain how to protect the JWT in the client side.
Assuming 100% TLS for all communication - both during and at all times
after login - authenticating with username/password via basic
authentication and receiving a JWT in exchange is a valid use case.
This is almost exactly how one of OAuth 2's flows ('password grant')
works.
[...]
You just set the Authorization header:
Authorization: Bearer <JWT value here>
But, that being said, if your REST client is 'untrusted' (e.g.
JavaScript-enabled browser), I wouldn't even do that: any value in the
HTTP response that is accessible via JavaScript - basically any header
value or response body value - could be sniffed and intercepted via
MITM XSS attacks.
It's better to store the JWT value in a secure-only, http-only cookie
(cookie config: setSecure(true), setHttpOnly(true)). This guarantees
that the browser will:
only ever transmit the cookie over a TLS connection and,
never make the cookie value available to JavaScript code.
This approach is almost everything you need to do for best-practices
security. The last thing is to ensure that you have CSRF protection on
every HTTP request to ensure that external domains initiating requests
to your site cannot function.
The easiest way to do this is to set a secure only (but NOT http only)
cookie with a random value, e.g. a UUID.
I don't understand why we need the cookie with the random value to ensure that external domains initiating requests to your site cannot function. This doesn't come free with Same-origin policy?
From OWASP:
Checking The Origin Header
The Origin HTTP Header standard was introduced as a method of
defending against CSRF and other Cross-Domain attacks. Unlike the
referer, the origin will be present in HTTP request that originates
from an HTTPS url.
If the origin header is present, then it should be checked for
consistency.
I know that the general recommendation from OWASP itself is Synchronizer Token Pattern but I can't see what are the vulnerabilities that remains in:
TLS + JWT in secure httpOnly cookie + Same-origin policy + No XSS vulnerabilities.
UPDATE 1:
The same-origin policy only applies to XMLHTTPRequest, so a evil site can make a form POST request easily an this will break my security. An explicit origin header check is needed. The equation would be:
TLS + JWT in secure httpOnly cookie + Origin Header check + No XSS vulnerabilities.
Summary
I had a misunderstood concepts about Same-origin policy and CORS that #Bergi, #Neil McGuigan and #SilverlightFox helped me to clarify.
First of all, what #Bergi says about
SOP does not prevent sending requests. It does prevent a page from
accessing results of cross-domain requests.
is an important concept. I thought that a browser doesn't make the request to the cross domain accordingly to the SOP restriction but this is only true for what Monsur Hossain calls a "not-so-simple requests" in this excellent tutorial.
Cross-origin requests come in two flavors:
simple requests
"not-so-simple requests" (a term I just made up)
Simple requests are requests that meet the following criteria:
HTTP Method matches (case-sensitive) one of:
HEAD
GET
POST
HTTP Headers matches (case-insensitive):
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type, but only if the value is one of:
application/x-www-form-urlencoded
multipart/form-data
text/plain
So, a POST with Content Type application/x-www-form-urlencoded will hit to the server (this means a CSRF vulnerability) but the browser will not make accessible the results from that request.
A POST with Content Type application/json is a "not-so-simple request" so the browser will make a prefligth request like this
OPTIONS /endpoint HTTP/1.1
Host: https://server.com
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: https://evilsite.com
Access-Control-Request-Headers: content-type
Accept: */*
Accept-Encoding: gzip, deflate, sdch
Accept-Language: es-ES,es;q=0.8
If the server respond with for example:
Access-Control-Allow-Origin: http://trustedsite.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type
Content-Type: text/html; charset=utf-8
the browser will not make the request at all, because
XMLHttpRequest cannot load http://server.com/endpoint. Response to
preflight request doesn't pass access control check: The
'Access-Control-Allow-Origin' header contains the invalid value
'trustedsite.com'. Origin 'evilsite.com' is therefore not allowed access.
So I think that Neil was talking about this when he pointed out that:
the Same-origin Policy only applies to reading data and not
writing it.
However, with the origin header explicit control that I proposed to Bergi I think is enough with respect to this issue.
With respect to my answer to Neil I didn't mean that that answer was the one to all my question but it remembered me another important issue about SOP and it was that the policy only applies to XMLHTTPRequest's.
In conclusion, I think that the equation
TLS + JWT in secure httpOnly cookie + Origin Header check + No XSS vulnerabilities.
is a good alternative if the API is in another domain like SilverlightFox says. If the client is in the same domain that the client I will have troubles with requests that doesn't include the Origin header. Again from the cors tutorial:
The presence of the Origin header does not necessarily mean that the
request is a cross-origin request. While all cross-origin requests
will contain an Origin header, some same-origin requests might have
one as well. For example, Firefox doesn't include an Origin header on
same-origin requests. But Chrome and Safari include an Origin header
on same-origin POST/PUT/DELETE requests (same-origin GET requests will
not have an Origin header).
Silverlight pointed this out to.
The only risk that remains is that a client can spoof the origin header to match the allowed origin, so the answer i was looking for was actually this
UPDATE: for those who watch this post, I have doubts about if the origin header is needed at all using JWT.
The equation would be:
TLS + JWT stored in secure cookie + JWT in request header + No XSS vulnerabilities.
Also, the previous equation has httpOnly cookie but this won't work if you got the client and the server in different domains (like many SPA application today) because the cookie wouldn't be sent with each request to the server. So you need access the JWT token stored in the cookie and send it in a header.
Why Same-origin policy isn't enough to prevent CSRF attacks?
Because the Same-origin Policy only applies to reading data and not writing it.
You want to avoid http://compromised.com from making a request like this (from the user's browser):
POST https://example.com/transfer-funds
fromAccountId:1
toAccountId:666
A legit request would look like this:
POST https://example.com/transfer-funds
fromAccountId: 1
toAccountId: 666
csrfToken: 249f3c20-649b-44de-9866-4ed72170d985
You do this by demanding a value (the CSRF token) that cannot be read by an external site, ie in an HTML form value or response header.
Regarding the Origin header, older browsers don't support it, and Flash had some vulnerabilities that let the client change it. Basically you'd be trusting Adobe not to screw anything up in the future...does that sound like a good idea?
ensure that you have CSRF protection on every HTTP request
You only need CSRF protection on requests with side-effects, such as changing state or sending a message
I just want to summarize the answers.
As other mentioned SOP applies only to XmlHttpRequests. This means by specification browsers must send ORIGIN header along with requests that were made by means of XmlHttpRequests.
If you check Chromium sends origin when you submit form as well. However this doesn't mean other browsers do. The image below illustrates two post requests made in Firefox. One is made by submitting a form and a second one using XHR. Both requests were made from http://hack:3002/changePassword to http://bank:3001/chanePassword.
Browser is allowed to not send the origin header if request was made from the same domain. So server should check the origin policy only when origin header is set.
The conclusion is: if you use cookies and a request comes to server without origin header, you can't differentiate whether it was made by submitting a form from another domain or by XHR within the same domain. That's why you need additional check with CSRF.
TLDR:
As long as the request is sent(with cookie), there is a possibility of an csrf attack.
SOP(Same-origin-Policy) only don't allow cross-origin reads(except for embedded element such as <script> <img> etc), but allow cross-origin writes.
More specifically, browser use CORS mechanism to get Cross-Origin resource, there is two situations:
Simple requests
Browser will add the Origin field, then send to the server.(csrf happen)
Others (Other than Simple requests)
CORS preflight was triggered, the request may not be sent to the server.(csrf may happen)
Reference
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
Related
I was researching the purpose of CORS headers, and the accepted answer here: What is the issue CORS is trying to solve? says, that the reason for its existence is, to prevent cookies unintentionally being sent to external sites when making HTTP requests from JS (fetch or XMLHttpRequest).
Reading up on how cookies are handled based on the Set-Cookie documentation page, doesn't the SameSite=Strict cookie option obsolete CORS completely? It says:
means that the browser sends the cookie only for same-site requests,
that is, requests originating from the same site that set the cookie.
If a request originates from a different domain or scheme (even with
the same domain), no cookies with the SameSite=Strict attribute are
sent.
In summary both CORS headers and the SameSite=Strict option for the Set-Cookie header seem to solve the same problem. Why does both exist?
I'm using Google Cloud CDN to cache an HTML page.
I've configured all the correct headers as per the docs, and the page is caching fine. Now, I want to change it so that it only caches when the request has no cookies, i.e. no cookie header set.
My understanding was that this was simply a case of changing my origin server to add a vary: cookie header to all responses for the page, then only adding the caching headers Cache-Control: public and Cache-Control: max-age=300 when no cookie header is set on the request.
However, this doesn't work. Using curl I can see that all caching headers, the vary: cookie header, are set as expected when I send requests with and without cookies, but I never get cache hits on the requests without cookies.
Digging into the Cloud CDN logs, I see that every request with no cookie header has cacheFillBytes populated with the same number as the response size - whereas it's not for the requests with a cookie header set with a value (as expected).
So it appears like Cloud CDN is attempting to populate the cache as expected for requests with no cookies, it's just that I never get a cache hit - i.e. it's just cacheFillBytes every time, cacheHit: true never appears in the logs.
Has anyone come across anything similar? I've triple-checked all my headers for typos, and indeed just removing the vary: cookie header makes caching work as expected, so I'm almost certain my configuration is right in terms of headers and what Cloud CDN considers cacheable.
Should Cloud CDN handle vary: cookie like I'm expecting it to? The docs suggest it handles arbitrary vary headers. And if so, why would I see cacheFillBytes on every request, with Cache-Control: public and Cache-Control: max-age=300 set on the response, but then never see a cacheHit: true on any subsequent request (I've tried firing hundreds with curl in a loop, it really never hits, it's not just that I'm populating a few different edge caches)?
I filed a bug with Google and it turns out that, indeed, the documentation was wrong.
vary: cookie is not supported by Cloud CDN
The docs have been updated - the only headers that can be used with vary are Accept, Accept-Encoding and Origin.
As per the GCP documentation[1], it is informed that Cloud CDN respects any Vary headers that origin servers include in responses. As per this information it looks like vary:cookie is supported by GCP Cloud CDN since any Vary header that the origin serves will be respected by Cloud CDN. Keep in mind though that this will negatively impact caching because the Vary header indicates that the response varies depending on the client's request headers. Therefore, if a request for an object has request header Cookie: abc, then a subsequent request for the same object with request header Cookie: xyz would not be served from the cache.So, yes it is supported and respected but will impact caching (https://cloud.google.com/cdn/docs/troubleshooting-steps?hl=en#low-hit-rate).
[1]https://cloud.google.com/cdn/docs/caching#vary_headers
tl;dr, an XHR client in domain A is sending a request to a server in domain B, server responds with a Set-Cookie with Domain=A (the client's domain, the XHR's Origin), all CORS headers set correctly, should it work?
It's well known that one can't set a cookie to another domain. ( How to set a cookie for another domain
However given the following scenario:
Actors:
Client in domain A, a web based client
Server in domain B, setup with CORS headers permitting A as origin, including Access-Control-Allow-Credentials set to true
Communication flow 1 (baseline):
Client is issuing a simple GET request to the Server
Server responds with a cookie, and sets the Domain property to be of the server (Domain=B)
Client is sending another HXR request and has withCredentials=true
The cookie is sent back to the server without any issues
Note: the cookie sent in step #1 is not showing in document.cookies, even if it was not set as httpOnly (since it doesn't
belong to the client's domain). Also attempts to get it from the xhr
via looking at the "Set-Cookie" header, you'll be blocked, by design:
https://fetch.spec.whatwg.org/#forbidden-response-header-name it will
even won't show in Chrome dev tools under the network tab! but it will
still be sent)
Communication flow 2 (my question):
Client is issuing a simple GET request to the Server
Server responds with a cookie, but sets the Domain property to be of the client (Domain=A)
Client is sending an HXR request and has withCredentials=true
The cookie is not sent back and doesn't seem to be stored anywhere
Why am I a bit surprised? Since the XHR origin is A and it requests something that sets the cookie to domain A (if I look in Postman I clearly see the Set-Cookie header being sent with Domain being the same as the request's Origin), and I have the most permissive CORS setting for that, what's the reasoning behind not letting me do it? (I was expecting it to fail, but still made me wonder)
Questions
Where is the best place in the spec/RFC that it clarifies that this won't work also for XHR where the cookie Domain equals the Origin
What is the attack vector in scenario 2 if theoretically the browser did allow the server to store the cookie if and only if the Origin is the same as the cookie Domain and the CORS origin allows that Origin.
Is there another way to make it work? Maybe it works but my POC was setup incorrectly?
Appendix: Reasoning
I'm looking for a way to have a cross origin CSRF using something like the Cookie to header token method, but due to the cross origin issue, it seems that it's impossible. The only workaround I thought of is sending the CSRF token as a header from the server, then the client can just save it as a cookie it can access later, is there any other way to do it? Is this considered secure?
A resource can only set cookies for its host's registrable domain. If Facebook were to use Google Fonts, and Google could use that to override Facebook cookies, that'd be pretty disastrous.
As for where this is defined, step 5 and 6 of https://www.rfc-editor.org/rfc/rfc6265#section-5.3 handle this. (Fetch largely defers to this RFC when it comes to interpreting the Set-Cookie header on responses.)
I've run into a few problems with setting cookies, and based on the reading I've done, this should work, so I'm probably missing something important.
This situation:
Previously I received responses from my API and used JavaScript to save them as cookies, but then I found that using the set-cookie response header is more secure in a lot of situations.
I have 2 cookies: "nuser" (contains a username) and key (contains a session key). nuser shouldn't be httpOnly so that JavaScript can access it. Key should be httpOnly to prevent rogue scripts from stealing a user's session. Also, any request from the client to my API should contain the cookies.
The log-in request
Here's my current implementation: I make a request to my login api at localhost:8080/login/login (keep in mind that the web-client is hosted on localhost:80, but based on what I've read, port numbers shouldn't matter for cookies)
First the web-browser will make an OPTIONS request to confirm that all the headers are allowed. I've made sure that the server response includes access-control-allow-credentials to alert the browser that it's okay to store cookies.
Once it's received the OPTIONS request, the browser makes the actual POST request to the login API. It sends back the set-cookie header and everything looks good at this point.
The Problems
This set-up yields 2 problems. Firstly, though the nuser cookie is not httpOnly, I don't seem to be able to access it via JavaScript. I'm able to see nuser in my browser's cookie option menu, but document.cookie yeilds "".
Secondly, the browser seems to only place the Cookie request header in requests to the exact same API (the login API):
But, if I do a request to a different API that's still on my localhost server, the cookie header isn't present:
Oh, and this returns a 406 just because my server is currently configured to do that if the user isn't validated. I know that this should probably be 403, but the thing to focus on in this image is the fact that the "cookie" header isn't included among the request headers.
So, I've explained my implementation based on my current understanding of cookies, but I'm obviously missing something. Posting exactly what the request and response headers should look like for each task would be greatly appreciated. Thanks.
Okay, still not exactly what was causing the problem with this specific case, but I updated my localhost:80 server to accept api requests, then do a subsequent request to localhost:8080 to get the proper information. Because the set-cookie header is being set by localhost:80 (the client's origin), everything worked fine. From my reading before, I thought that ports didn't matter, but apparently they do.
I have a HTTP "rest" web service API that I am creating for accessing user data.
The web service integrates with a XACML policy decision point. The policy decision point determines if the request is authorized as a function of the user and the data being accessed. Generally, the response is "permit" (action is allowed) or "deny" (action is not allowed).
If permit, then the call continues and data is returned.
If deny, then the call is aborted and an HTTP 403 (forbidden) is returned.
However, certain policies have "obligations" to indicate that the action is conditionally allowed with further action. The analogy that I generally use is the "this credit card transaction may be allowed, but the clerk needs to view the customer's ID, and then make this call again asserting that the ID matches the credit card".
In my web service, I want to prompt the client to take action and include additional information in the request URL to indicate that the obligation was met. I am communicating this information with structured body response understood by the client application.
My question is what is the appropriate HTTP status code to use in the scenario.
"403" would not be appropriate (text from HTTP spec Authorization will not help and the request SHOULD NOT be repeated).
My best guess would "401" (unauthorized), but I am not sure if this status code is specifically around the use of the authorization header and username/password type of concerns.
Maybe you could use the HTTP status code 303 or 307 to point the user to a temporary redirect location that somehow encodes the extra "obligation"?
A 401 response seems reasonable to me. The HTTP authentication mechanism is extensible, so you could conceivably create a custom authentication scheme so that you are properly setting the WWW-Authenticate header as required by the RFC. Browsers won't know what to do with your custom scheme, but I assume your clients aren't browsers anyway, if they have to understand what an obligation is.
Example request sequence:
PUT /some/resource/that/has/obligations HTTP/1.1
Content-Type: application/json
Authorization: token my-oauth-token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: obligation urn:my-app:my-obligation;param1;param2
PUT /some/resource/that/has/obligations HTTP/1.1
Content-Type: application/json
Authorization: token my-oauth-token
Authorization: obligation urn:my-app:my-obligation:result=ok
HTTP/1.1 201 Created
Location: /some/resource/that/has/obligations/1
Another option would be to return a 202 on the initial post or put, and then confirm it on a later post. This option would require a little more state management server side, since you'd first accept the operation, and then wait for the client to confirm it.
PUT /some/resource/that/has/obligations HTTP/1.1
Content-Type: application/json
Authorization: token my-oauth-token
HTTP/1.1 202 Accepted
Location: /some/resource/that/has/obligations/1
X-Obligation: urn:my-app:my-obligation;param1;param2
POST /some/resource/that/has/obligations/1 HTTP/1.1
Content-Type: application/json
Authorization: token my-oauth-token
X-Obligation: urn:my-app:my-obligation;result=ok
HTTP/1.1 200 OK
One thing to always keep in mind with obligations, though... they're always enforced client-side, so unless the obligation involves hitting another service that your service can double-check against, you never know if the client actually performed the obligation. If you don't control the client, then obligations are really just theatre.