Another Intro for Cookies

肥仔John發表於2021-12-14

Cookies are strings 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

  1. The name=value pair, after encodeURIComponent, should not exceed 4KB. So we can not store anything huge in cookie.
  2. 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(like Tue, 19 Jan 2038 03:14:07 GMT), which we can use date.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 to expires 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.
  • Lax, as the default option of Chrome. It follows the rules like below

    • link <a href="..."></a>, send cookie
    • pre-render <link rel="prerender" href="..."/>, send cookie
    • GET form <form method="GET" action="...">, send cookie
    • POST form <form method="POST" action="...">, cookie sending is forbidden
    • iframe <iframe src="..."></iframe, cookie sending is forbidden
    • image <img src="...">, cookie sending is forbidden
    • AJAX and Fetch, cookie sending is forbidden
  • None, 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

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:

<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.

How to mitigate CSRF attacks ?

  1. Check the origin
    We can check one or both of 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.
  2. Anti-CSRF tokens(two different ways but remain the same principle)

    1. 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.
    2. 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.

The downsides of Anti-CSRF tokens

  1. 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.
  2. 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:

  1. A page at site.com loads a banner from another site: <img src="http://ads.com/banner.png">
  2. Along with the banner, the server at ads.com may set the Set-Cookie header with a cookie, which originates from ads.com domain, such as id=123.
  3. Next time when ads.com is acccessed, the remote server gets the id cookie and recognizes the user.
  4. 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.
  5. The cookie belongs to ads.com here to site.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
  })
}

相關文章