Cookies are string
s of data that are stored directly in the browser. They are a part of HTTP protocol, defined by RFC 6265 specification.
Cookies are often set by server using the response Set-Cookie
HTTP-header. Then, the browser automatically attaches them to every request to the same domain using the request Cookie
HTTP-header.
name=value
pair, after encodeURIComponent
, should not exceed 4KB. So we can not store anything huge in cookie.A domain defines where the cookies are accessible, but in practice though, we can not set any domain within limitations.
By default, a cookie is accessible only the domain that set it. So, if the cookie was set by site.com
, we can not get it from other.com
, nor can subdomain like forum.site.com
by default! That's tricky but a safety restriction to allow us store some how sensitive data in cookies, which be sure that should be available only on one site.
If we'd like to allow subdomains such as forum.site.com
to get a cookie, we should explicitly set the domain
option to the root domain domain=site.com
or the old notation domain=.site.com
for very old browsers for historical reasons.
document.cookie = `author=john; domain=.site.com`
The url path prefix must be absolute(that starts with /
). It makes the cookies accessible for pages under the path. By default, it's the current path.
For example, if a cookie is set with path=/admin
, it's visible at pages /admin
and /admin/something
, but not at /
or /home
.
Usually, we set path
to the root as below, which can be accessed from the whole website.
document.cookie = `author=john; path=/`
By default, the so-called session cookies disappear when the browser closes. To let cookies survive from a browser close, we can set either the expires
or max-age
option.
The HttpOnly flag is an optional flag that can be included in a Set-Cookie
response header to tell the browser to prevent client side script from accessing the cookie.
The biggest benefit here is protection against XSS(Cross-Site Scripting). If a site has an XSS vulnerability then an attacker could exploit this to steal the cookies of a visitor, essentially taking over their session and logging in the victim's account.
When a piece of JavaScript attempts to read a cookie with the HttpOnly flag set, a empty string will be returned instead of the cookie itself.
Unless you have a specific requirement to access the cookie with client-side script, you should enable this flag.
It instructs the browser that the cookie must only ever be sent over a secure connection. You can see it on the end of this header: Set-Cookie: CookieName=CookieValue; path=/; Secure
Because a session cookie is incredibly sensitive, it should not be sent over an insecure connection as it would be trivial for an attacker to intercept it and abuse it. If you serve your site over HTTPS then you should set this flag on your cookies.
The new property SameSite is used to avoid CSRF and user tracking.
There are three options for SameSite
Strict
, prohibit request along with any cookie belongs to the target website, when the target URL and source are not the same. Set-Cookie: CookieName=CookieValue; SameSite=Strict;
Lax
, as the default option of Chrome. It follows the rules like below <a href="..."></a>
, send cookie<link rel="prerender" href="..."/>
, send cookie<form method="GET" action="...">
, send cookie<form method="POST" action="...">
, cookie sending is forbidden<iframe src="..."></iframe
, cookie sending is forbidden<img src="...">
, cookie sending is forbiddenNone
, disable SameSite feature, but as a premise, we should enable Secure feature first. Set-Cookie: CookieName=CookieValue; SameSite=None;
Set-Cookie: CookieName=CookieValue; SameSite=None; Secure
Cross-Site Request Forgery, also known as CSRF or XSRF, has been around basically forever, as old as the web itself. It stems from a simple capability that a site has to issue a request to another site. Let's have little example:
<form action="https://your-bank.com/transfer" method="POST" id="stealMoney">
<input type="hidden" name="to" value="fsjohnhuang">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.querySelector('#stealMoney').submit()
</script>
Assuming the above site is running on https://evil-hacker.com
, and what it does is forging a request that is being sent cross-site to your e-bank. And the real problem is that the browser will send our cookies belong to your-bank.com
with the request. And the request will pass through all authority you currently hold in this time, which means if you're logged in your e-bank you just donated to me. If you weren't logged in then the request would be harmless.
Origin
header and Referer
header to see if the request originated from a different origin to your own, these values indicate where the request came from. if the request was cross-origin you simply throw it away. They do get protection from browsers to prevent tampering, but they may not always be present either. For example: origin: https://hi.com
, referer: https://hi.com/login
.The downsides of Anti-CSRF tokens
The above methods have given us robust protection against CSRF for a long time. Checking the Origin and Referer headers isn't 100% reliable and most sites resort to some variation of the Anti-CSRF token approach.
Third-party cookies are traditionally used for tracking and ads services, due to their nature. Naturally, some people don't like being tracked, the browsers allow us to disable such cookies.
An instance for third-party cookie:
site.com
loads a banner from another site: <img src="http://ads.com/banner.png">
ads.com
may set the Set-Cookie
header with a cookie, which originates from ads.com
domain, such as id=123
.ads.com
is acccessed, the remote server gets the id
cookie and recognizes the user.ads.com
is capable of recognizing the visitor and tracking the view path.ads.com
here to site.com
or others is called third-party cookie.The value of document.cookie
consists of name=value
pairs, delimited by ;
. Each one is a separate cookie. For example:
console.log(document.cookie) // cookie1=value1; cookie2=value2; ...
Since document.cookie
is an accessor property, an assignment to it is treated specially. A write operation to document.cookie
updates only cookies mentioned in it, but doesn't touch others
console.log(document.cookie) // cookie1=value1; cookie2=value2;
// specify cookie1 with options
document.cookie = `cookie1=value3; domain=.site.com; path=/`
console.log(document.cookie) // cookie1=value3; cookie2=value2;
// cookie.ts
const COOKIE = document.cookie as const
const encode = encodeURIComponent as const
const decode = decodeURIComponent as const
export enum SameSite {
LAX = 'Lax'
STRICT = 'Strict'
NONE = 'None'
}
export interface Options {
maxAge?: number
expires?: Date
domain?: string
path?: string
httpOnly?: boolean
secure?: boolean
sameSite?: SameSite
}
export function get(name: string): string | void {
const cookies = COOKIE.split(';').reduce((accu, str) => {
const [ k, v ] = str.split('=')
accu[k] = v
return accu
}, {})
return name in cookies ? decode(cookies[name]) : undefined
}
export function set(name: string, value: string, opts?: Options) {
const cookie = [`${encode(name)}=${encode(value)}`]
if (opts) {
if ('maxAge' in opts) {
cookie.push(`max-age=${opts.maxAge}`)
}
if (opts.expires) {
cookie.push(`expires=${opts.expires.toUTCString()}`)
}
if (opts.domain) {
cookie.push(`domain=${(opts.domain[0] === '.' ? '' : '.') + opts.domain}`)
}
if (opts.path) {
cookie.push(`path=${opts.path}`)
}
if (opts.httpOnly) {
cookie.push(`httpOnly`)
}
if (opts.secure) {
cookie.push(`secure`)
}
if (opts.sameSite) {
cookie.push(`sameSite=${SameSite[opts.sameSite]}`)
}
else {
cookie.push(`sameSite=${SameSite[SameSite.Lax]}`)
}
}
document.cookie = cookie.join(';')
}
export function delete(name: string) {
set(name, null, {
maxAge: -1
})
}