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.
Limitations
- The
name=value
pair, afterencodeURIComponent
, should not exceed 4KB. So we can not store anything huge in cookie. - The total number of cookies per domain is limited to around 20+, the exact limit depends on the browser.
domain
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`
path
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=/`
Expires and max-age
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.
expires
, the cookie expiration date defines the time, when the browser will automatically delete it. The date must be exactly in the GMT timezone(likeTue, 19 Jan 2038 03:14:07 GMT
), which we can usedate.toUTCString
to get. For instance, we can set the cookie to expire in a day:let date = new Date(Date.now() + 86400e3) document.cookie = `author=john; expires=${date.toUTCString()}`
If we set
expires
to a date in the past, the cookie is deleted.max-age
, is an alternative toexpires
and specifies the cookie's expiration in seconds from the current moment. If set to zero or a negative value, the cookie is deleted:document.cookie = `author=john; max-age=0`
HttpOnly flag
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.
Secure 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.
SameSite
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.- example:
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
- Downside: To strict to cause when you redirect to GitHub from other site, you will have to login again, even though you have login before. That means all SSO will be unavailable.
- example:
Lax
, as the default option of Chrome. It follows the rules like belowNone
, disable SameSite feature, but as a premise, we should enable Secure feature first.- invalid setting:
Set-Cookie: CookieName=CookieValue; SameSite=None;
- effective setting:
Set-Cookie: CookieName=CookieValue; SameSite=None; Secure
- invalid setting:
CSRF/XSRF (Cross-Site Request Forgery)
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:
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.
How to mitigate CSRF attacks ?
- Check the origin
We can check one or both ofOrigin
header andReferer
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
. Anti-CSRF tokens(two different ways but remain the same principle)
- embedding a random token into the from
when genuine user submits this form, the random token will return back, and server-side checks if it matches the one issued for this form before. since in the CSRF attack scenario, attacker can never get the random token through such as AJAX from page which is constrained by Same Origin Policy. - besides embedding a random token into the from, issuing a cookie contains the same value
the genuine user submits the random token and the cookie back to server-side, and server-side verify the submission. The value of cookie can be not equal to that of the input control, maybe the value of the input control is a hash value from the cookie one.
- embedding a random token into the from
The downsides of Anti-CSRF tokens
If user open multiple tabs for the same document, the follow-up cookie value will cover the previous.
- Solution: It's preferred to store the value into session.
The attackers can not get the anti-csrf tokens from the front-end, but it's possible to get it done on server-side. Request the target URL from server-side to get the anti-csrf token and cookie value, and put them as response. And there is risk for users of submitting data with valid anti-csrf token.
- Solution: Connect the anti-csrf token with the user login identity, while the user login identity is stored on server-side where is tricky to get for attacker.
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
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.
- Safari does not allow third-party cookies at all.
- Firefox comes with a black list of third-party domains where it blocks third-party cookies.
An instance for third-party cookie:
- A page at
site.com
loads a banner from another site: - Along with the banner, the server at
ads.com
may set theSet-Cookie
header with a cookie, which originates fromads.com
domain, such asid=123
. - Next time when
ads.com
is acccessed, the remote server gets theid
cookie and recognizes the user. - What's even more important is, when the user moves to another site which has the same banner, then
ads.com
is capable of recognizing the visitor and tracking the view path. - The cookie belongs to
ads.com
here tosite.com
or others is called third-party cookie.
Cookie Manipulation by JavaScript
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;
Helper functions
// 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
})
}