Cross-origin Resource Sharing — A Hands-on Tutorial (Part III : Cookies)
Part II of the tutorial dealt with complex CORS requests and pre-flight
check by the browsers. In this final part, we look at dealing with cookies in CORS . We will also look at subtle differences between same site and same origin and how it impacts cookie behaviour.
Cookies
By default, Cookies are neither set nor sent in CORS Requests. Let’s see that in a bit more detail.
The Cookie Demo Page
- Its a simple HTML page served by the PageServer, which allows you to play with different scenarios
- What we are going to do is first make a request to APIServer for user “john” and expect the server to set a cookie “visited-userid=john”
- The next request to the APIServer will make a
Fetch
call with/@me
and it expects the APIServer to read thevisited-userid
cookie from the request . - If successful, we should see “john” JSON data displayed on the page . If the server doesn’t find the cookie in the incoming request it will simply return
{"error":"user not found"}
with 404 status code
Run the page and API Servers
$ go run pageserver.goand in different window
$ go run apiserver/allow_creds/apiserver.go
- Point your browser to http://pageserver.cors.com:12345/cookiedemo.html
- Send the first request by hitting
Set Cookie Via AjaX
button - This action does a fetch request to the URL
http://apiserver.cors.com:12346/users/john
with the fetch configured to include credentials
fetch(url, {
credentials: 'include',
....}
Observations
- Here is what you would see in debugger/dev tools
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://apiserver.cors.com:12346/users/john. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).
Security Risk ⚠️ Even though the response was blocked from rendering, the request indeed made it to the Server and was processed. You can confirm this by looking at the stdout of ApiServer as well as dev tools Network call.
Lets fix it —
- Stop the earlier running ApiServer and now run it in “allow” mode
$ go run apiserver/allow_creds/apiserver.go --allow-creds
- Hit the
Set Cookie via Ajax
button again and you would see a response now, the JSON data for user John{“UserName”:”jdoe”,”FirstName”:”John”,”LastName”:”Doe”,”Country”:”France”}
- The only change in Server was adding
Access-Control-Allow-Credentials header
whenallow-creds
param is set.
if *allowCreds {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
Response Headers -
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://pageserver.cors.com:12345
Content-Type: application/json
Set-Cookie: visited-userid=john; SameSite=Strict
Date: Fri, 05 Jun 2020 07:32:26 GMT
Content-Length: 74
Now let’s do the second request by hitting the Use Cookie Set Earlier
button. This next request to the APIServer makes a Fetch
call with /@me
and expects the APIServer to read the visited-userid
cookie from the request and return “john” data with 200 OK status . This works as well ☀️
So we have successfully set a cookie, as well as read it from the incoming request. On to another interesting/confusing aspect
Same- Site is != Same-Origin
Readers paying close attention may have noticed that theSet-Cookie
header returned by the APIServer had an attribute SameSite=Strict
.
If you set
SameSite
toStrict
, your cookie will only be sent in a first-party context. I.e. cookie will only be sent if the site for the cookie matches the site currently shown in the browser's URL bar.
The URL of the page is http://pageserver.cors.com:12345/cookiedemo.html where as the request was sent to http://apiserver.cors.com:12346/users/john
So why was the cookie sent?
Well its because they are cross-origin but same-site 🙀 . The same or cross site distinction is based on “effective TLD” . Or in user terms, pageserver.cors.com and apiserver.cors.com have same value i.e. cors.com
.
🔗 Checkout this page to understand the rules
You can see this in action by making a couple of changes
- Create a
cross-site
server. Bind a new loopback IP toapiserver.sscors.com
on your machine.
For example this entry in my /etc/hosts file.127.0.0.4 apiserver.sscors.com
Notice that the value for server issscors.com
which is different from cors.com
(different values for eTLD+1
)
- Run the API Server in cross-site mode (add
--cross-site
command line param)
$ go run apiserver/allow_creds/apiserver.go --allow-cred --cross-site
On the cookiedemo.html
page, go to the Cross Site demo section and hit the “Set Cookie Via Ajax Fetch” button. The request succeeds and indeed the response has Set-Cookie header
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://pageserver.cors.com:12345
Content-Type: application/json
Set-Cookie: visited-userid=john; SameSite=Strict
Date: Fri, 05 Jun 2020 08:28:34 GMT
Content-Length: 74
Now lets hit the Use Cookie Set Earlier
button but this returns error !
{"error": "user not found"}
Why?
A look at the request dump in server or browser dev tools tells the story
GET /users/@me HTTP/1.1
Host: apiserver.sscors.com:12346
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.5
Cache-Control: no-cache
Connection: keep-alive
Origin: http://pageserver.cors.com:12345
Pragma: no-cache
Referer: http://pageserver.cors.com:12345/cookiedemo.html
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:77.0) Gecko/20100101 Firefox/77.0
🔎 Notice that -
- The
visited-userid
cookie was not included in the request since theSameSite=Strict
attribute was set on the cookie. - There was no issue in setting the cookie but it won’t be included in the cross-site GET or POST request with SameSite as
Strict
- The server still has
Access-Control-Allow-Credentials: true
for cross-origin requests, but its overridden by same-site cookie setting
Can we “fix” it?
While its generally a bad idea to allow cross-site cookies, if you did want to enable that, change the cookie attribute toSameSite=None
.
- Run the API Server to set same site as none (add
--same-site-none
command line param)
$ go run apiserver/allow_creds/apiserver.go --allow-creds --cross-site --same-site-none
Now make the first request to set the cookie again. The response header from server shows the cookie attribute as none
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://pageserver.cors.com:12345
Content-Type: application/json
Set-Cookie: visited-userid=john; SameSite=None
Date: Fri, 05 Jun 2020 08:46:12 GMT
Content-Length: 74
- Now hitting the button,
Use Cookie Set Earlier
, would show john’s data
🔥
SameSite=None
opens the door for CSRF and other vulnerabilities and you should consider its use carefully .⚠️This example above may not work in future since browser can potentially discard this setting on an HTTP connection . e.g. warning in firefox — Cookie “visited-userid” will be soon rejected because it has the “sameSite” attribute set to “none” or an invalid value, without the “secure” attribute.