Introduction of cookies
The HTTP protocol is stateless, but cookies can be used to maintain “session state” between the client and server.
In simple terms, the server sets the Cookie to the client through the set-cookie response header, and the client adds a request header named Cookie to the server next time it sends a request to the server to carry the content “buried” by the server before, so that the server can identify the client.
Take a simple 🌰 :
/ / the server
const http = require("http");
http
.createServer((req, res) = > {
if (req.url == "/") {
res.end("hello world");
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.writeHead(200The [["Set-Cookie"."name=haochuan9421"]./ / set the cookie
]);
res.end("some data");
}
})
.listen(80);
Copy the code
/ / the client
var xhr = new XMLHttpRequest();
xhr.open('GET'."/someapi");
xhr.send();
Copy the code
When the client makes a request again, it automatically carries the Cookie that was “buried” before:
With a brief introduction to cookies, let’s take a look at its SameSite property.
SameSite properties
SameSite has three optional values:
Strict
Lax
None
.
Starting with Chrome 80, not specifying SameSite is equivalent to setting Lax. You can disable this behavior with Chrome ://flags/#same-site-by-default-cookies. If you disable SameSite, it is equivalent to setting it to None. The differences between them will be introduced later in the context of specific scenarios.
Taking a look at the concept of third-party in the image above, what is a third party to a Cookie?
For example, let’s say we’re visiting a website called ‘bar.com’, and when we import images from ‘foo.com’, the image service sets a Cookie, we call it a ‘third party Cookie’. In the new version of Chrome, you can set “third-party cookies” only if you set the SameSite property of cookies to None and the Secure property to true (more on that later). Users can block “third-party cookies” in their browser preferences.
To put it simply: in the case that the currently visited website and the website requesting service are “Cross sites”, the Cookie set by the third-party service is called “third-party cookies”.
Cross-site is determined not by the same origin policy (protocol, host, port), but by PSL (common suffix list). For example, ‘foo.example.com’ and ‘bar.example.com’ are not ‘cross-site’ because they are both part of example.com and are ‘co-site’. IO and bar.github. IO are both subdomains of ‘github. IO ‘, but they are cross-site, because ‘github. You can see which domain names belong to PSL here.
This is essentially the same as setting the Domain property for cookies. We all know that parent cookies can be set for subdomains, for example, a request for ‘foo.example.com’ can set cookies for Domain ‘.example.com’. IO ‘cannot set cookies for Domain ‘.github. IO ‘. It’s like you can’t set the Cookie’s ‘Domain’ to ‘.com’. Because ‘.com’ and ‘github. IO ‘are both in PSL.
For a more authoritative explanation, see “same-site” and “cross-site” Requests here
The port is different. For example, our website is Bar.com :8080. We will not judge the image of Bar.com :9000 as a third party when we introduce it.
If the Scheme is different, the third party is identified. For example, if our website is ‘bar.com’, the images of ‘bar.com’ will be judged as a third party when we introduce them. But in Chrome you can ignore the protocol restrictions with Chrome ://flags/#schemeful-same-site.
Cookies themselves are port – and protocol-neutral (Scheme).
In addition to the scenario of loading images from third-party websites, AJAX/ FETCH requests to third-party websites, iframe embedded in third-party websites, form submission to third-party websites, and link jump to third-party websites may all involve “third-party cookies”. What are the effects of SameSite values on these scenarios where “third-party cookies” are possible? Let’s explore one by one (multi-picture warning 😀) :
1. The AJAX requests
When we send an AJAX request across domains, our request cannot be sent due to the same origin policy of the browser:
However, we can use CORS to solve the cross-domain problem:
const http = require("http");
http
.createServer((req, res) = > {
if (req.url == "/") {
res.end("hello world");
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.writeHead(200The [["Set-Cookie"."name=haochuan9421"]./ / set the cookie
["Access-Control-Allow-Origin"."*"].// Allow cross-domain requests
]);
res.end("some data");
}
})
.listen(80."0.0.0.0");
Copy the code
However, when we make the request again, although there is a Cookie set in the response header of the cross-domain request, we find that the next request will not carry the Cookie set by the previous server.
One problem with this is that we lose the ability to maintain server-client “session state” using cookies. So how do you carry cookies when making requests to third-party websites? The following conditions must be met:
- Websites enable HTTPS and set the Secure property of cookies to true
- Access-control-allow-origin is set to a specific Origin, not *
- Set access-control-allow-credentials to true
- The SameSite property is set to None
If you want to test this code locally, note that both www.foo.com and www.bar.com requests are made to the service. This can be done easily by modifying the hosts file on your computer. The HTTPS certificate is a self-signed certificate generated by mkcert.
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
if (req.url == "/") {
res.end("hellow world");
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.writeHead(200The [["Set-Cookie"."name=haochuan9421; Secure; SameSite=None"],
...(req.headers.origin // Cross-domain request headers contain origin, the website from which the request originated? [["Access-Control-Allow-Origin", req.headers.origin], * cannot be used, must be specified
["Access-Control-Allow-Credentials"."true"].// Set to allow cross-domain requests to carry cookies] : []),]); res.end("some data");
}
}
)
.listen(443."0.0.0.0");
Copy the code
After the above conditions are met, cross-domain requests can carry cookies:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET'."https://www.bar.com/someapi");
xhr.send();
Copy the code
All four conditions are indispensable:
When HTTPS is not enabled:
When Secure is not set:
When access-Control-allow-origin is set to *
When the value of access-Control-allow-credentials is not true
When the SameSite property is set to Strict or Lax
The same is true for sending requests using the fetch API of the browser. When making cross-domain requests using fetch, you need to set the “credentials” to “include” if you want to carry cookies:
fetch("https://www.bar.com/somedata", {
"method": "GET"."credentials": "include"
})
Copy the code
2. Set a third-party IFrame
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
console.log(req.headers.host);
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type"."text/html; charset=utf-8");
res.end(` < div > this is the parent page < / div > < iframe SRC = "https://www.bar.com/" > < iframe > `);
} else {
res.writeHead(200The [["Set-Cookie"."name=haochuan9421; Secure; SameSite=None"],
["Content-Type"."text/html; charset=utf-8"]]); res.end(This is the child page ); }}else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443."0.0.0.0");
Copy the code
If SameSite is set to Strict:If SameSite is set to Lax:If SameSite is not specified:If SameSite is set to None:
This indicates that cookies are valid when cross-domain iframe pages are imported only if SameSite is explicitly specified as None.
For example: if we want to embed Bilibili’s video player on our website, we can’t use 1080p image quality by importing bilibili’s video player directly into our website via iframe.
<iframe
src="//player.bilibili.com/player.html?bvid=BV1Vv41157uK&high_quality=1"
allowfullscreen="allowfullscreen"
width="100%"
height="500"
scrolling="no"
frameborder="0"
></iframe>
Copy the code
This is because the SameSite property of the Cookie at site B is not set to None. When it is embedded in other third-party websites, the player at site B cannot transfer the Cookie to the server, and the server cannot get the user’s login state. For users who have not logged in, Site B does not provide 1080p playback.
In Chrome, however, we can set third-party cookies to None by disabling Chrome ://flags/#same-site-by-default-cookies. When we disable this option and restart the browser, The embedded IFrame can play 1080p station B video (provided that station B has been logged in).
3. Load third-party images or scripts
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
console.log(req.headers.host, req.url);
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type"."text/html; charset=utf-8");
res.end(` < div > this is the parent page < / div > < img SRC = "https://www.bar.com/" > < / img > `);
} else {
res.writeHead(200The [["Set-Cookie"."name=haochuan9421; Secure; SameSite=Strict"],
["Content-Type"."image/png"]]); fs.createReadStream("logo.png").pipe(res); }}else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443."0.0.0.0");
Copy the code
This is the same as importing a third-party iframe. Cookies are valid only if the SameSite property is None.
An example of an application: Below is a site with Google ads, and you can see that the Google ad-related Cookie sets the SameSite property to None. That way, when enough sites incorporate Resources such as Google’s AD scripts, he can build up a user’s browsing trajectory and preferences for each site, and deliver ads with precision.
4. Submit the form to a third-party website
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type"."text/html; charset=utf-8");
res.end(`
);
} else {
console.log(req.headers.host, req.url, req.method, req.headers.cookie);
res.writeHead(200The [["Set-Cookie"."name=haochuan9421; Secure; SameSite=Strict"]]); res.end("ok"); }}else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443."0.0.0.0");
Copy the code
As you can see from the above tests, setting SameSite to None is a dangerous behavior that makes it very easy to launch cross-site Request Forgery (CSRF) attacks against your site, Because requests to your site from a third party malicious site also carry cookies, a fake request can be identified as an ordinary user request. Let’s assume that www.foo.com is a malicious site and www.bar.com is our own:
The examples in this section are illustrative only and show some key steps. Details such as the implementation of login and login verification will be simplified
// This is our own normal website
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
if (req.url == "/") {
// There is a transfer form on the home page of our website
res.setHeader("Content-Type"."text/html; charset=utf-8");
res.end(
');
} else if (req.url == "/login") {
// After login, the client stores the user's Cookie information
res.setHeader("Set-Cookie"."name=haochuan9421; Secure; SameSite=None");
res.end("login success");
} else if (req.url == "/transfer") {
// Users who have logged in can transfer funds, but those who have not logged in cannot transfer funds
res.end(req.headers.cookie ? "ok" : "fail");
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443."0.0.0.0");
Copy the code
The user directly visits www.bar.com to submit the transfer form, and the failure will be prompted if there is no login (no Cookie), so the user will first enter www.bar.com/login to login, and there will be Cookie on the client after login. When the user returns to the home page and submits the transfer form again, the transfer will be successful. This simulates a simple cookie-based authentication site.
Let’s take a look at how the attacker broke the authentication drip of www.bar.com. When the attacker to know your site have the function of transfer, then he can be induced into the prepared malicious sites, in this malicious sites to your web site launched a transfer request, if before entering a malicious web site login your website and login state has not expired, then the bogus requests will be successfully transferred the money users. Here is the code for the malicious website:
// This is a malicious site that wants to fake requests
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
if (req.url == "/") {
res.setHeader("Content-Type"."text/html; charset=utf-8");
res.end(` < div > this is a malicious web site < / div > < form id = "fake - form" action = "https://www.bar.com/transfer" method = "post" target = "submit - target" > `);
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443."0.0.0.0");
Copy the code
As can be seen, after the user is induced to enter the malicious website, the malicious website automatically initiates a forged transfer request to your server. Since the SameSite attribute in your Cookie is set to None, this forged request will also carry the user’s Cookie. The interface authentication based solely on cookies is breached, exposing users’ funds to security risks. This is a big reason why the latest versions of browsers change the default value of SameSite from None to Lax.
5. Links to third-party websites
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),},(req, res) = > {
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type"."text/html; charset=utf-8");
res.end(`<div>foo page</div>
<a href="https://www.bar.com/">www.bar.com</a>`);
} else {
console.log(req.headers.host, req.url, req.headers.cookie);
res.writeHead(200The [["Set-Cookie"."name=haochuan9421; Secure; SameSite=None"],
["Content-Type"."text/html; charset=utf-8"]]); res.end("bar page"); }}else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443."0.0.0.0");
Copy the code
Strict This rule is too Strict and can result in a very poor user experience. For example, if there is a GitHub link on the current webpage, users will not have GitHub cookies when they click the jump, and the jump will always be unlogged.
conclusion
The default value of the SameSite attribute for cookies in modern browsers is pretty reasonable. As a website owner, we don’t usually need to set this attribute manually, and it’s only reasonable to set it when our service needs to connect with “third parties”.
Strict: indicates that the third-party Cookie is strictly prohibited. Cookies are carried only when the URL of the current web page is consistent with the request target, which is generally used to ensure system closure and security.
Lax is the default of most modern browsers, and it can also avoid some bad user experience, such as no login when jumping from other websites.
None is the most relaxed setting, and is usually used to open up our services to different third parties while also tracking users, such as advertising, for open security purposes.