takeaway

This article, based on [RFC 6265], briefly explains the use and features of cookies. It probably includes the following four contents: 1) Introduce the use of Cookie; 2) Explain the format of Cookie; 3) Test Cookie’s response in various situations; 4) CSRF attack description

Environment, tools, and prior knowledge

  1. System:macOS Mojava
  2. IDE:IDEA
  3. SwitchHosts
  4. OpenSSL
  5. Chrome
  6. jsbasis
  7. NodeJSbasis
  8. HTTPbasis

0x001 Use of Cookie

Using cookies is very simple and can be summarized in 4 steps:

  1. The front-end sentHTTPRequest to the back end
  2. Back-end generation is to be placed incookieAnd set to the responseSet-CookieThe head
  3. The front-end pulls out of the responseSet-CookieSave the contentcookieInformation to local
  4. The front-end continues to access the back-end page, which will be saved locallycookiePut in the requestedCookieThe head

Illustrate with a picture:

With code:

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Set-Cookie': 'name=123'})
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)
Copy the code

The function of this code is simple:

  1. To create aHTTPThe server
  2. Add one for each responseSet-CookieThe head, its value is zeroname=123
  3. The body of the response is onehtmlOnly one of themscriptTag, the script inside the tag willcookieOutput to the current page

Start the script, then open your browser and visit:

$ node index.js
$ open http://localhost:3000
Copy the code

You can see:

  1. Display on the pagename=123It is usSet-CookieThe content of the
  2. HTTPresponseThe head isSet-Cookie: name=123

Open Application > cookies > localhost:3000 and you can see the cookie we set:

Open another TAB and then visit the page (or refresh it, but open another TAB for comparison) :

As you can see, there is an extra Cookie in the request compared to the first access, and the contents of the Cookie are the contents of our set-cookie (this does not mean that Cookie === set-cookie, The content of the Cookie comes from set-cookie. The conversion from set-cookie to Cookie will be explained later.

This is the simplest way to use cookies.

Format description of 0x002 cookies

As you can see from Chrome’s cookies management tool, a cookie has many attributes:

  • Name
  • Value
  • Domain
  • Path
  • Expire / Max-Age
  • Size(Ignore, should only be front-end statistics Cookie key value pair length, such as “name” and “123” length is 7, if you know, please inform)
  • HttpOnly
  • Secure
  • SameSite

These properties will be explained slowly in the following sections.

1. Expire

Expires is used to set an expiration date for a cookie. It’s a UTC time after which the cookie is no longer valid and the browser will not include the expired cookie in the cookie request header. And the expired cookie is deleted.

Code:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if(! cookie || ! cookie.length){ res.writeHead(200, {'Set-Cookie': `name=123; expires=${date.toUTCString()}`})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)

Copy the code

The above code adds an extra expires attribute to the set-cookie in response, which is used to Set the expiration time. Here it is Set to one minute later:

    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
Copy the code

We also make a judgment that if there is a cookie in the request, we do not add the set-cookie header, otherwise it will never expire.

Open your browser (delete previous cookies first) and go to localhost:3000:

Date: Sun, 01 Dec 2019 15:24:47 GMT
Set-Cookie: name=123; expires=Sun, 01 Dec 2019 15:25:47 GMT
Copy the code

The set-cookie format was changed to include an expires attribute, and the time is 1 minute after the Date attribute. Within this minute, if we refresh the page, we will find that there is a Cookie in request and no set-cookie in response:

After 1 minute, a new set-cookie is generated, and the Cookie in the request is missing:

The expires attribute is in the format:

    expires-av = "Expires="Sanal-cookie-date sanal-cookie-date = < RFC113-date, defined in [RFC2616], Section 3.3.1>Copy the code

Sana-cookie-date is a time format that looks something like this:

    Sun, 06 Nov 1994 08:49:37 GMT
Copy the code

Js can be obtained using:

    $ new Date().toUTCString()
    "Sun, 01 Dec 2019 15:18:12 GMT"
Copy the code

The default expires value is session, which is deleted when the current browser closes.

2. Max-Age

Max-age is also used to set the cookie expiration time, but it sets the number of seconds relative to the time the resource was fetched. For example, if the response Date is Sun, 01 Dec 2019 15:44:43 GMT for the first time, the cookie will expire at Sun, 01 Dec 2019 15:45:43 GMT

On the first request, you can see that the cookie format is: set-cookie: name=123; Max-age =60, one more max-age=60.

If accessed within 60 seconds, you will see that the request contains a Cookie: name=123 and the response does not have a set-cookie

After 60 s, a new cookie is created.

Max-age defaults to session and is deleted when the current browser is closed.

The format of max-age is:

    max-age-av = "Max-Age=" non-zero-digit *DIGIT
Copy the code

3. Domain

Domain limits the Domain name under which this cookie is included in the cookie header of the request.

For example, if we set a cookie with the Domain attribute example.com, the user agent will send the cookie to example.com, www.example.com, This Cookie is carried in the request Cookie at www.corp.example.com.

The code:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if(! cookie || ! cookie.length){ res.writeHead(200, {'Set-Cookie': 'name=123; domain=example.com'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)
Copy the code

Here we add a domain=example.com. Then use SwitchHost to configure several hosts:

127.0.0.1 example.com
127.0.0.1 www.example.com
127.0.0.1 www.corp.example.com
Copy the code

Access example.com:3000, which is returned when accessed for the first time

Set-Cookie: name=123; domain=example.comCopy the code

Then we refresh the page and see that the cookie is carried to the request:

Visit www.example.com:3000 and this cookie also exists

Visit www.corp.example.com:3000, there are the cookies

But if you access exampel2.com:3000, it doesn’t exist, and because the domain in the cookie is different from the current domain, the user agent refuses to store the cookie, so we don’t see the output of the cookie:

cookie
cookie

The default value of domain is the current domain name.

The format for domain is:

    domain-av = "Domain=" domain-value
Copy the code

4. Path

While Domain is limited to the cookie’s Domain name, Path is limited to the cookie’s Path. For example, if we set the cookie’s Path to /a, it will not be available at/or /b

The code:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if(! cookie || ! cookie.length){ res.writeHead(200, {'Set-Cookie': 'name=123; path=/a'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)

Copy the code

Add a path=/a, nothing else changes, call localhost:3000/a, get a cookie:

Refresh the page and send the cookie you just got:

Access /a/b, but can:

Access /b, no cookie is sent, and a new cookie is generated:

This path was rejected because it did not match the current path:

If/is accessed, the same result is obtained as if /b is accessed:

Therefore, Path can restrict which Path and its children a cookie can use. Ancestor and sibling paths are not allowed. If this value is not available, it is /

The default value for Path is /, which is valid for the entire site.

The format of path is:

    path-av = "Path=" path-value
Copy the code

5. Secure

Secure is used to indicate that a cookie is sent only over a “Secure” channel, which generally means HTTPS. This means that it is sent only when HTTPS is used, not HTTP.

The code:

let https = require("https");
let http = require("http");
let fs = require("fs");

const options = {
    key: fs.readFileSync('./server.key'),
    cert: fs.readFileSync('./server.pem')}; const app = (req, res) => { const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if(! cookie || ! cookie.length){ res.writeHead(200, {'Set-Cookie': 'name=123; secure'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}

https.createServer(options, app).listen(443);
http.createServer(app).listen(80);
Copy the code

Port 80 and port 443 are used to provide HTTP and HTTPS services. The certificates required for HTTPS services are generated by OpenSSL, which is not mentioned here. Visit https://example.com to get cookies:

Refresh the page and send cookies:

Visit http://example.com to get cookies:

But rejected:

The default value of Secure is false, that is, HTTP/HTTPS access.

The secure format is:

    secure-av = "Secure"
Copy the code

6. HttpOnly

Secure Indicates that cookies can only be used over Secure channels. HttpOnly indicates that cookies can only be used over Http. We’ve been accessing cookies on the front end through document.cookie, but if we specify this, we can’t manipulate cookies through Document. cookie.

The code:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if(! cookie || ! cookie.length){ res.writeHead(200, {'Set-Cookie': 'name=123; httpOnly'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)
Copy the code

HttpOnly: localhost:3000

Refresh, send cookies:

Use document.cookie to manipulate cookies:

> document.cookie = 'name=bar'
< "name=bar"
> document.cookie
< ""
Copy the code

If you look at the cookie, you can see that value is still 123, and HttpOnly is checked.

The default value for httpOnly is fasle, which means cookies can be manipulated using document.cookie

The format of httpOnly is

    httponly-av = "HttpOnly"
Copy the code

7. SameSite

Notice, before WE get to this property, it’s important to note that the cookie in the request header is not sent only when the url is entered in the address bar, but when the HTTP request is sent across all pages of the browser, such as the SRC of IMG, Script SRC, link SRC, ifram SRC, and even ajax configuration, if the conditions mentioned above are met, the HTTP request will contain the cookie of the request address, even if you are visiting from another website. This is why CSRF is available.

The code:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host}`})}if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/"/>
        `)
    }
    res.end()
}).listen(3000)

Copy the code

If the accessed address begins with example, a cookie is set with the name host and the value is the current accessed address. If the accessed address begins with localhost, return three elements whose SRC points to example.com:3000, three different resources (which don’t exist, but are sufficient). Visit example.com:3000 and get cookie:host=example.com :3000

Refresh the page and send cookies normally:

Localhost :3000. Because of the previous code, script, img, iframe will be returned, and the user agent will load these three resources:

When you open these three resources, you can see that each resource carries a cookie.

  • script.js

  • img.jpg

  • index.html

SameSite is designed to limit this:

Strict

When SameSite is Strict, the user agent will only send cookies that match the exact URL of the site, not even the subdomain.

The code:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host}; SameSite=Strict`}) }if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/index.html"/>
        `)
    }
    res.end()
}).listen(3000)

Copy the code
  • Access example.com:3000 to get cookies:

  • Refresh normal sending cookies:

  • Accessing localhost:3000 any resource that points to example.com:3000 does not send cookies:

  • Includes jumping past from this page:

  • subdomain

Lax

The default SameSite for newer browsers is Lax. If SameSite is Lax, it will be reserved for cross-site sub-requests, such as image loads or calls to frames, but will only be sent when the user navigates to a URL from an external site. Such as link link. (I have not been able to test it out, specifically look at ruan Yifeng -Cookie’s SameSite attribute and MDN-HTTP cookies)

The code:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host}; SameSite=Lax`}) }if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
            <form action="http://example.com:3000/form" method="post">
                <button>post</button>
            </form>
            <form action="http://example.com:3000/form" method="get">
                <button>get</button>
            </form>
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/index.html"/>
        `)
    }
    res.end()
}).listen(3000)

Copy the code

Here are my results

  • A Link forward: with cookies
  • The form [action = get] : take cookies
  • Form [action=post] : no cookie
  • Iframe: does not contain cookies
  • Script: no cookie
  • Img: No cookies

That is, the simple navigation jump band, and the resource load does not take.

None

On newer browsers, if you want cookies to support both same-site and cross-site Settings, you need to explicitly specify SameSite=None (my test results are consistent with not adding this attribute).

The code:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host}; SameSite=None`}) }if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
            <form action="http://example.com:3000/form" method="post">
                <button>post</button>
            </form>
             <form action="http://example.com:3000/form" method="get">
                <button>get</button>
            </form>
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/index.html"/>
            
        `)
    }
    res.end()
}).listen(3000)
Copy the code

8. Set-cookie format

In fact, the format of set-cookie is composed of Cookie key-value pair and attribute list, so it can be expressed as follows (without ABNF) :

    Set-Cookie: cookie-pair ";" cookie-av ";" cookie-av....
Copy the code

Cookie-pair is a format similar to name=123 written above:

    cookie-pair = cookie-name "=" cookie-value
Copy the code

This can be followed by a set of properties, cookie-pair and cookie-av; And cookie-AV can be expressed as:

    cookie-av = expires-av / max-age-av / domain-av /
                path-av / secure-av / httponly-av /
                extension-av
Copy the code

The expires-av, max-age-av, domain-av, path-av, secure-av, and httponly-av correspond to each of the above attributes, but there are some differences. Chrome implements the same Origin attribute. However, RFC 6265 does not have this property, which can be viewed as extension-AV,

Do the correspondence again:

  • Name: cookie – Name
  • Value: the cookie – Value
  • Domain: Domain – the av
  • Path: the Path – the av
  • Expire/max-age: Expires -av/max-age-av
  • Size (ignore, should only be front-end statistics Cookie key value pair length, such as “name” and “123” length is 7, if any big god knows, please inform)
  • HttpOnly: HttpOnly – av
  • Secure: Secure – av
  • SameSite: the extension – the av

You can see that the format of cookies is not the same as that of cookies in Chrome, it tiled cookie-pair and cookie-av.

0x003 CookieIn a variety of situations

1. In each status codeSet-CookieWill it all be dealt with? including301,404,500?

According to RFC 6265, the user agent may ignore status codes contained at level 100, but must process set-cookies (both level 400 and level 500) in other responses.

According to the tests, in addition to 100,101, a 407 (for unknown reasons) does not process set-cookies, and even 999 does

2. How to send multiple cookies to set-cookie, and how to deal with cookies?

The code:

const http = require('http');

http.createServer((req, res) => {
    res.setHeader('Set-Cookie'['cookie1=1'.'cookie1=2'.'cookie2=2'.'cookie3=3'])
    res.end()
}).listen(3000)
Copy the code

Call localhost:3000 and get cookie:

As you can see, cookies are placed in multiple set-cookie headers. In FACT, RFC 7230 states that there should not be duplicate headers in a packet. If a header has multiple values, it should be separated by, Like the accept-encoding and Accept-language below. The problem is, it can exist as a valid value for cookie-value, but dropping it as a delimiter will break cookie parsing 😢, so there you go.

Refresh the sent Cookie, and you can see that the Cookie is folded, and the repeated cookie-name Cookie is overwritten in order:

3. How does Ajax send cookies? Will set-cookies returned by Ajax be accepted?

The answer is yes, using axios as an example:

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Set-Cookie': 'name=ajax'})
    res.write(`
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script >
        axios.get('http://example.com:3000/', {
            withCredentials: true
        })        
        </script>
    `)
    res.end()
}).listen(3000)
Copy the code

Call localhost:3000 and get cookie:

Check the cookie store, it has been saved:

View the Ajax request that has been sent:

The same origin policy is followed. In the case of the same origin, the packet is sent by default. In the case of cross-domains, you need to add withCredentials: True. The XMLHttpRequest and Fetch configurations may differ.

Off the top of my head

0x004 CSRF

example

  • Open the login pagelocalhost:3000/loginAfter logging in, the back end returns oneSet-Cookie: name=bob, back-end readCookieDetermine if the user is logged in. If logged in, the amount of the account will be displayed, along with a transfer form

  • Transfers are made throughhttp://localhost:3000/transfer?name=lucy&money=1000To make the transfer,nameIs the target user of the transfer,moneyIs the amount. Here for convenience, use directlyGET.

  • Start a new service, pretend to be another site, this site is only oneimgtheimgsrcIt points to the transfer address above:
const http = require('http');
http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type':'text/html'})
    res.write(`
        <img src="http://localhost:3000/transfer? name=bad&money=10000000">
    `)
    res.end()
}).listen(3001)
Copy the code
  • Has been inlocalhost:3000Log inbobaccessexample.com:3001, you will find that although only a picture points tolocalhost:3000But because it’s availablelocalhost:3000cookieThat’s why it’s taken there, and this address is based oncookieDetermine the user and perform the transfer operation:

  • If Bob returns to his account page, he is bankrupt and restricted to high spending:

defense

(This article mainly talks about cookie, CSRF principle and defense is not the focus)

  • detectionReferrerHeader: But some browsers can disable this header (I can’t find one that does either)
  • SameSite: Not yet universal
  • csrftoken: generalcsrfLaunch attacks are similar to the above, set up a phishing site, in fact, the phishing site is unable to access the source stationcookie, sendcookieIs browser native behavior, so just generate a randomtokenJust bring it with you when each form is sent, because phishing sites can’t predict thistokenThe value of the. You can even take thistokenDirectly stored incookieIs retrieved and sent with the form when it is sent. It can also be injected into a form when the backend generates itinputp[typehidden]The domain.
  • Secondary authentication, such as verification code, payment password, etc

0 x005 resources

  • Example source code
  • RFC 6265-HTTP State Management Mechanism
  • MDN HTTP Cookie
  • Ruan Yifeng -Cookie SameSite

0 x006 take goods

React is a phenomenal micro scene editor.