Securing Third Party Cookies

Boris Reitman
6 min readApr 2, 2021

This is a technical article concerning web development.

Third party cookies help advertisers track people’s interests in order to show relevant ads. Given a choice between irrelevant versus relevant ads, I chose later. Thank you, third party cookies.

A third party cookie is any cookie set on an embedded resource inside a web page. If your web page has an image ad that is loaded from an advertising web server on another domain, then the ad may change based on who is browsing.

There is a security issue with third party cookies called Cross Site Request Forgery (CSRF), which arises whenever a third party web API relies on a cookie to authenticate the request. Third party cookies allow an attacker to construct a fishing web page that would trigger API operations on behalf of a user. The defense is to design an API which in addition to the cookie also expects a nonce. A legitimate nonce, also known as a CRSF token, is made available only to a legitimate page and is difficult to steal. Another defense is not to use cookies at all but to pass an authentication token in HTTP headers when making a request to the server using JavaScript.

There are, therefore, several reasons why one would be against third party cookies. Security experts think that cookies create too large an attack surface to warrant their use. Privacy activists are against tracking people while they browse. Finally, the anti-capitalists are against the profit motive and wish to penalize advertisers for their success.

In reality, third party cookies are great precisely because they allow the advertisers to make money. This is what sponsors the Internet, and it is the reason why websites like YouTube, GMail, Google Docs and Facebook are free.

Yet web browsers have moved to curb the use of third party cookies by requiring developers to jump through many hoops. Google Chrome announced that it will drop support to such cookies in a few years. Safari has a setting “Prevent cross-site tracking” enabled by default, which in addition to blocking third-party cookies, strips off paths from the “Referrer” HTTP header in cross-site setting.

Safari has “Prevent cross-site tracking” by default

Yet, those who are leading these efforts do not realize that there are other ways to track people, such as browser’s Local Storage. Thus, all those efforts to ban cookies only make lives of developers difficult. Cookies do have a unique feature that can not be replicated: (a) they are sent on the first request from the browser to the web server, and (b) they do not require any JavaScript. All other methods require using JavaScript to read browser state and then add unique identifier to all subsequent requests.

Thus, if a publisher would only embed an image from the advertiser but not dynamic content such as JavaScript or an Iframe, then the advertiser needs cookies to work in order to show relevant ads. If third party cookies are not available, the market will respond by moving to new technical solutions.

The publishers want to monetize website traffic, and the advertisers want to show relevant ads that convert. Therefore, the publishers would agree to requirements from advertisers to include advertiser’s JavaScript in publishers’ pages. If done incorrectly, it makes security actually worse, because it opens the door to JavaScript back doors which can steal users’ passwords and authentication tokens.

Yet, there is a way to use third party cookies securely. The example scenario I will demonstrate is a web page publisher.tld/landing.html that loads advertiser’s JavaScript API hosted at advertiser.tld. The secure way to include third party JavaScript in the landing page, is to load it with integrity verification.

<script src="https://advertiser.tld/api/v1/api.js"
integrity="sha256-..."
crossdomain="use-credentials"></script>

The integrity attribute is used by the publisher in order to have the assurance that he won’t be loading JavaScript backdoors if the advertiser’s API is hacked.

Also, whenever the integrity attribute is used an a cross-domain setting, the crossdomain attribute is required. The publisher should set it to use-credentials in order for the third party cookies to work. (The other value is anonymous and as the name denotes, it would deny the advertiser the ability to track users.)

In order for the API to work cross domain, several Access Control HTTP headers must be set. The following is an example in Node:

function add_access_control_headers(req, headers){
headers['Access-Control-Allow-Origin' ] =
req.headers.origin||"*";
headers['Access-Control-Allow-Methods'] =
'GET,PUT,POST,OPTIONS,HEAD,DELETE';
headers['Access-Control-Allow-Headers'] =
'Content-Type, X-Requested-With, Cache-Control';
headers['Access-Control-Allow-Credentials'] = 'true';
}

The Access-Control-Allow-Credentials is needed whenever the crossdomain attribute has use-credentials value.

In order to track users, the web server of the advertiser issues a cookie user_id to the browser. Every API request needs to have that cookie set. To make this work, Google Chrome requires advertiser’s web server to set the cookie with Secureand SameSite=None flags. For example:

Set-Cookie: user_id=foo; Path=/; Domain=provider.tld; Max-Age=86400; Secure; SameSite=None;

If advertiser’s JavaScript does not need to access the cookie value at all, the the web server can add HttpOnly flag to the above Set-Cookie header for increased protection from CSRF attacks. But if the advertiser’s JavaScript needs to read the cookie, there are two solutions. One of them is to create an API endpoint that returns the cookie value in the HTTP response body. For example,

// server code
function get_cookie_handler(req, res){

var headers = { 'Content-Type': "application/json" };
add_access_control_headers(req, headers);
res.writeHead(200, headers);
var cookies = parse_cookies(req);
res.write({user_id: cookies['user_id']})
res.end();
}
// browser code
function get_cookie(){
var url = "https://advertiser.tld/api/v1/cookie";
return fetch(url).then(r => r.json()).then(r => r.user_id);
}

Another solution to expose the cookie to the JavaScript is to use an Iframe. The Iframe receives the cookie and pass it to the parent page. The advertiser hosts a file get_cookie.html shown below. His api.js which is embedded into publishers page, creates the Iframe.

<!-- get_cookies.html: load this page in an iframe --><!doctype html>
<script>
var value = get_cookie('user_id');
parent.postMessage({
message: "advertiser_cookies",
cookies: {
user_id: get_cookie('user_id')
// add additional cookies to expose
}
}, "*");
function get_cookie(name) {
var parts = ("; "+document.cookie).split("; "+name+"=");
if (parts.length === 2) return parts.pop().split(';').shift();
}
</script>

Note that only specific cookies are exposed, rather than all of the cookies sent by the web server. This protects those other cookies from being stolen via Cross Site Scripting (XSS) and fishing attacks.

The corresponding code in the api.js file adds the iframe to the page, and listens to the message event. It should create an iframe with the appropriate sandbox attribute in order to protect the publisher from the case that the advertiser was hacked.

function create_iframe(url){
var iframe = document.createElement("iframe");
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
iframe.src = url;
iframe.style.display = 'none';
return iframe;
}

The allow-scripts and allow-same-origin relaxations to the strict sandbox are necessary permissions for the get_cookie.html to work.

Next, implement a function in api.jsthat waits for the cookies message using a Promise, so that it can be conveniently embedded in other code.

function get_cookies(){  var url="https://advertiser.tld/get_cookies.html";
var iframe = create_iframe(url);
document.body.appendChild(iframe);

return new Promise(function(resolve){
function handler(event){
var data = event.data;
if (typeof data === 'object' && data !== null){
if (data.message=='advertiser_cookies'){
resolve(data.cookies);
return true;
}
}
return false;
}
window.addEventListener("message", handler);
});
}
async function test(){
var cookies = await get_cookies();
console.log(cookies.user_id);
}
test();

As already mentioned, the security risk in exposing cookies to the publisher, is exposing them to fishing pages and XSS attacks. One way to protect from this is not to expose any critical cookies. Alternatively, expose a derived value from the cookie, such as a hash, instead of the original cookie. This would allow to distinguish between code running in the publisher’s context from the code running in advertiser’s context, and would allow to set appropriate level of authorization.

For the suggested scheme to work the server would need to maintain a reverse map from hashes back to the original cookie values. However, this can be avoided by hashing only part of the cookie and using the unmodified part for the lookup. In a 32 character random cookie the first 16 characters may be used as an identifier and the remaining 16 characters as the authentication token. The exposed cookie value would be modified to replace the last 16 characters with their SHA-256 hash truncated to 16 characters. In pseudo-code,

// server issues cookie
var cookie = generate_random_string(32);
await record_issued_cookie(cookie.substr(0,16), cookie);
headers['Set-Cookie'] = `user_id=${cookie}; ...`
// server verify
var cookie = get_cookie(req, 'user_id')
var lookup = cookie.substr(0,16)
var check = cookie.substr(16);
var original = await get_issued_cookie(lookup);
var auth = null;
if (original == cookie){
auth = "full";
} else if (check == sha256(original.substr(16)).substr(0,16)){
auth = "embedded";
}

In closing, there is a way to use cookies safely. Often, cookies are the best tool for the job and web developers should not forget that it is the advertising industry which is writing their paychecks.

--

--

Boris Reitman

The course of history is determined by the spreading of ideas. I’m spreading the good ones.