If you have any questions during reading, please let me know in the comments section. All comments will be replied to

Since the release of Chrome 80 earlier this year, Google chrome has changed the default Cookie policy: The default value of the SameSite property has been changed to Lax.

Gold nine silver ten, no problem of the harvest season. Some people got the offer, some people got flowers, and worse still, they received milk tea in autumn. In front of the thriving scene let me this ten thousand years single dog also began to mind, mobile phone suddenly shivered, pulled my heartstrings. Could it be…

To my surprise, WTM received this…

Dear Employee: After scanning by the security team, the XXX business line you are in charge of has CSRF vulnerability, which is now ordered to be repaired within a time limit. Repair time 2020.09.25. Please repair in time and contact the security group for reinspection.... Jiangnan Tannery safety groupCopy the code

U1S1 although safety related interview questions back very 6 but play real combat…

But on second thought, as a development siege lion with development experience. In the face of difficulties, or to pretend as always calm and calm. Who goes to hell if I don’t?

See the leadership suspicious expression, my heart is actually this style son…

Old rules, encounter a problem affirmation is to learn this problem first, then I opened Baidu

What is thecsrf

Cross-site request forgery Cross-site Request Forgery, also known as one-click attack or session riding, usually abbreviated as CSRF or XSRF, Is a method of hijacking a user to perform unintended actions on a currently logged Web application. In contrast to cross-site scripting (XSS), which exploits the user’s trust in a given site, CSRF exploits the site’s trust in the user’s Web browser.

— from Baidu Baike

According to the summary in Baidu Encyclopedia, cross-site request forgery means three: cross-site request forgery. To implement the cross-site premise, we create a site…

Create a targeted website (fat and full)

  • First, we create a directory and add related files. The directory structure is as follows
    ├ ─ ─ frond - end front-end code directory │ ├ ─ ─ index. The HTML user homepage │ └ ─ ─ the login. The login HTML page ├ ─ ─ package - lock. Json NPM ├ ─ ─ package. The json NPM └ ─ ─ ├── ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ index.js ├─ ├─ index.js ├─ ├─ index.js ├─ ├─ index.js The back-end library entry file from which the methods in the library are exportedCopy the code
  • Second, write the project code
    • The first step is to implement the simplest login page login.html
      <! DOCTYPEhtml>
      <html>
          <head>
              <title>Login - Fat belly full</title>
          </head>
          <body>
              <h1>User login, fat belly ~</h1>
              <form id="form" method="POST" action="/api/login">
                  <input required name="id" placeholder="Please enter user ID" />
                  <button type="submit">The login</button>
              </form>
          </body>
      </html>
      Copy the code
    • The second step is to realize the front-end page index.html of the user’s home page
      <! DOCTYPEhtml>
      <html lang="en">
      <head>
          <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
          <title>Welcome to use - fat and full</title>
      </head>
      <body>
          <h1>Fat fills the belly, and fat supports the belly</h1>
          <h3>welcome<span id="user"></span>. Wallet balance:<span id="money"></span>yuan</h3>
          <form>
              <label>Transfer to:</label>
              <input name="toUser" placeholder="Who's the baby?">
              <input name="money" placeholder="Transfer Amount">
              <button type="submit">transfer</button>
          </form>
          <script>
              const refresMyMoney = () = > {
                  $.get('/api/appinfo').done(({ errno, data }) = > {
                      if (errno === 0) {
                          const { userName, money } = data
                          $('#money').html(money)
                          $('#user').html(userName)
                      } else {
                          location.href = '/login.html'}})} $('form').on('submit'.e= > {
                  e.preventDefault()
                  const formData =
                      $(e.target).serializeArray()
                          .map(({ name, value }) = > ({ [name]: value }))
                          .reduce((memo, curItem) = >({... memo, ... curItem }), {}) $.get('/api/transfer', formData).done(resp= > {
                      if (resp.errno === 0) {
                          refresMyMoney()
                      } else {
                          location.href = '/login.html'}})})// When the page is loaded, request the current page data
              window.onload = refresMyMoney
          </script>
      </body>
      </html>
      Copy the code
    • Step 3, implement the back-end logic server.js
      const Koa = require('koa')
      const KoaRouter = require('koa-router')
      const path = require('path')
      const koaBody = require('koa-body')
      const koaStatic = require('koa-static')
      
      const { db, getReqData } = require('./utils')
      
      const PORT = 2333
      const app = new Koa()
      const router = new KoaRouter()
      
      router.all('/api/login'.ctx= > {
          const { body: { id } } = getReqData(ctx)
          if (db[id]) {
              ctx.cookies.set('userid', id)
              ctx.redirect('/')}else {
              ctx.redirect('/login.html')
          }
      })
      
      router.all('/api/appinfo'.ctx= > {
          const { id } = getReqData(ctx)
          if (db[id]) {
              ctx.body = {
                  errno: 0.data: {
                      money: db[id],
                      userName: id
                  }
              }
          } else {
              ctx.body = { errno: Awesome! }
          }
      })
      
      router.all('/api/transfer'.ctx= > {
          const { query: { toUser, money }, id } = getReqData(ctx)
          if(! id) { ctx.body = {errno: Awesome!.errmsg: 'You are not logged in please log in' }
              return
          }
          db[toUser] += (+money)
          db[id] -= (+money)
          ctx.body = { errno: 0 }
      })
      
      app.use(koaBody())
      app.use(router.routes(), router.allowedMethods())
      app.use(koaStatic(path.resolve(__dirname, '.. /frond-end')))
      
      app.listen(PORT, () = > {
          console.log(`the server is running in ${PORT}`)})Copy the code
    • Finally, implement the backend tool library
      // db.js
      module.exports = {
          quanquan: 100.zhangsan: 100.huangguan: 100
      }
      
      // getReqData.js
      module.exports = (ctx) = > {
          const {
              query,
              request: {
              body
              }
          } = ctx
          const id = ctx.cookies.get('userid')
          const token = ctx.headers.token
          return { query, body, id, token }
      }
      
      // index.js
      exports.getReqData = require('./getReqData')
      exports.db = require('./db')
      Copy the code
  • Then, install the dependencies and modify thempackage.jsonThe specific steps are as follows:
    • performnpm init -yQuickly initialize the Node project
    • performnpm i koa koa-body koa-router koa-staticInstallation project dependencies
    • performnpm i nodemon -DInstallation development dependency
    • inpackage.jsonOf the filescriptAdd under field"start": "nodemon server/server.js"
    • Well done, buddy…
  • The lastnpm start, browser accesslocalhost:2333, see the following page, it is successful ~

At this point, we have the core functionality of (so far) the website of the world’s largest IPO company. Soon to ring the bell, and a little bit excited ~

To make our site look bigger, we added a NB domain name to it, modified the hosts file, and added the following two lines

127.0.0.1 zhifubao.com
127.0.0.1 huangguan.com
Copy the code

After the addition, we can access the website through Zhifubao.com. You can experience the two browsers login zhangsan and Quanquan respectively, and experience the mutual transfer function between them, as shown below

Current step code address

Launch the first wave of attacks

After a meal operation, we have been attacked website — fat belly full, the following began to attack operation. As summarized in Baidu Baike. As a cross-site scripting attack, the first thing to do is cross-site. Next we create an attack site, create a directory hacker and create an index.html file that says:

<! DOCTYPEhtml>
<html>
<head>
    <meta charset="UTF-8">
    <title>----</title>
    <style>
        html.body.iframe { margin: 0; padding: 0; width: 100%; height: 100%; }</style>
</head>
<body>
    <iframe src="/" frameborder="0"></iframe>
    <img src="http://zhifubao.com:2333/api/transfer?toUser=quanquan&money=10">
</body>
</html>
Copy the code

Then, install NPM i-g live-server globally and execute live-server in the hacker directory. 127.0.0.1:8080 is the host of our attack

Now, zhang SAN, a diligent student, stays up all night to learn the basic principles and architecture ideas of the Linux kernel. All of a sudden I got an E-mail.

This is daytime, encounter this kind of mail that must be a swipe close popover. But zhang SAN, who had been studying hard all night, was wondering.

So the layout of the browser looks like this.

Shy student Zhang SAN felt ashamed when he saw the page and closed it quickly. Refresh the browser. 10 bucks short, 10 bucks extra in the ring account next door.

Eat mute kui of very uncomfortable, dare not stretch again.

Open the console and review what you just did.

There is a request from Zhibubao.com in the console of hacker website, and it also carries cookies. Poor Zhang SAN was attacked by the simplest CSRF even though he closed the page quickly. Let’s summarize the attack process:

  • First, guideFat belly usersaccessHacker web site
  • Second, through the page insideimgThe label toFat belly fullsendGETRequest and carryFat belly fullEverything under the siteCookie
  • Finally, the request is approved and the transfer is completed

The code address of the step so far

Defend against IMG tag GET attacks

When you think about the img tag on your hacker website, you’ll only send a GET request, so why not change the transfer interface to POST?

Talk is cheap…

The front-end code

// Change the request mode to POST
$.post('/api/transfer', formData).done(resp= > {
Copy the code

The back-end code

router.post('/api/transfer'.ctx= > {
    const { body: { toUser, money }, id } = getReqData(ctx)
    if(! id) { ctx.body = {errno: Awesome!.errmsg: 'You are not logged in please log in' }
        return
    }
    db[toUser] += (+money)
    db[id] -= (+money)
    ctx.body = { errno: 0}})Copy the code

Refresh the page to test the transfer function again, and it still works at 😂

Then open up Hacker and try…

CSRF cross-site request forgery is so easy to solve.

No worries, the long night zhang SAN decided to…

Defend against form attacks

Bug easily fixed, eating hot pot singing, happy happy…

By modifying the request method this is blocking many entry-level script kids. But when you meet hacker’s boss, you’ll get nothing but a shrug…

Find interface 404 hacker and update the code briefly

<body>
  <iframe src="/" frameborder="0"></iframe>
  <form action="http://zhifubao.com:2333/api/transfer" method="post">
    <input type="hidden" name="toUser" value="quanquan">
    <input type="hidden" name="money" value="10">
  </form>

  <script>
    document.addEventListener('DOMContentLoaded'.() = > {
      document.forms[0].submit()
    })
  </script>
</body>
Copy the code

Then, zhang SAN, who visited the page again, found that the page had jumped directly and the circle on the left had added another 10 yuan to the account

Zhang SAN feel not good, the website of a famous far – known unexpectedly so random jump. Backhanded to the relevant department to report the hacker website. So the hackers upgraded the site again.

<body>
  <iframe src="/" frameborder="0"></iframe>

  <iframe src="/" frameborder="0" style="display: none;" name="hacker"></iframe>
  <form action="http://zhifubao.com:2333/api/transfer" method="post" target="hacker">
    <input type="hidden" name="toUser" value="quanquan">
    <input type="hidden" name="money" value="10">
  </form>

  <script>
    document.addEventListener('DOMContentLoaded'.() = > {
      document.forms[0].submit()
    })
  </script>
</body>
Copy the code

The code diff can view this submission

POST is supposed to be more secure than GET.

Ultimate solution CSRF

After the previous steps, we can summarize the essence of CSRF attack in two aspects:

  1. The user is accessing the needCookieA web site that records login status
  2. Use the same browser to access links to sites that have constructed malicious requests

In view of these two points, it is not difficult for us to conclude that if we want to prevent being attacked, we can work from two levels.

As the user

As users, we have the option of using an infrequently used browser when visiting an indescribable site. It is best to uninstall the browser after using it…

As a developer

As a developer, we know that CSRF vulnerability only exists when a website requires cookies to record login status. Is there a way to bypass cookies?

Suppose, when we log in successfully, the server does not write cookies but sends a token to the browser. When the front end determines the successful login, it stores the returned token in localStorage (remember it is not a cookie). When the front-end sends a subsequent request to the back-end, the token is fetched from localStorage and written to the request header. The backend verifies the request header to determine the user login status. This seems to bypass the problem of cookies storing login state.

The whole process is as follows:

talk is cheap… , first front-end modification

index.html

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  <style>
    html.body { padding: 0; margin: 0; display: flex; align-items: center; flex-direction: column; padding-top: 100px; }
  </style>
  <title>Welcome to use - fat and full</title>
</head>
<body>

  <h1>Fat fills the belly, and fat supports the belly</h1>
  <h3>welcome<span id="user"></span>. Wallet balance:<span id="money"></span>yuan</h3>
  <form>
    <label>Transfer to:</label>
    <input type="text" name="toUser">
    <input type="text" name="money">
    <button type="submit">transfer</button>
  </form>
  <script>
    const refresMyMoney = () = > {
      $.ajax({
        url: '/api/appinfo'.headers: {
          token: localStorage.getItem('token')
        }
      }).done(({errno, data}) = > {
        if (errno === 0) {
          const {userName, money} = data
          $('#money').html(money)
          $('#user').html(userName)
        } else {
          location.href = '/login.html'}})} $('form').on('submit'.e= > {
      e.preventDefault()
      const formData =
        $(e.target).serializeArray()
          .map(({name, value}) = > ({[name]: value}))
          .reduce((memo, curItem) = >({... memo, ... curItem}) , {}) $.ajax({url: '/api/transfer'.method: 'post'.data: formData,
        headers: {
          token: localStorage.getItem('token')
        }
      }).done(resp= > {
        if (resp.errno === 0) {
          refresMyMoney()
        } else {
          location.href = '/login.html'}})})window.onload = refresMyMoney
  </script>
</body>
</html>
Copy the code

login.html

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    html.body { padding: 0; margin: 0; display: flex; align-items: center; flex-direction: column; padding-top: 100px; }
  </style>
  <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  <title>Login - Fat belly full</title>
</head>
<body>
  <h1>User login, fat belly ~</h1>
  <form>
    <input
      required
      name="id"
      placeholder="Please enter user ID"
      list="users"
    />
    <button type="submit">The login</button>
  </form>
  <script>
    $('form').on('submit'.e= > {
      e.preventDefault()
      const formData =
        $(e.target).serializeArray()
          .map(({ name, value }) = > ({ [name]: value }))
          .reduce((memo, curItem) = >({... memo, ... curItem }), {}) $.post('/api/login', formData).done(resp= > {
        if (resp.errno === 0) {
          const {data} = resp
          localStorage.setItem('token', data);
          location.href = '/'
        } else {
          alert('Login failed ~')}})})</script>
</body>
</html>
Copy the code

And then put in our back-end changes

const Koa = require('koa')
const KoaRouter = require('koa-router')
const path = require('path')
const koaBody = require('koa-body')
const koaStatic = require('koa-static')
const { db, getReqData } = require('./utils')
const PORT = 2333

const app = new Koa()
const router = new KoaRouter()
router.all('/api/login'.ctx= > {
  const { body: { id } } = getReqData(ctx)
  if (db[id]) {
    ctx.body = {
      errno: 0.data: id
    }
  } else {
    ctx.body = { errno: Awesome! }
  }
})
router.all('/api/appinfo'.ctx= > {
  const { token } = getReqData(ctx)
  if (db[token]) {
    ctx.body = {
      errno: 0.data: {
        money: db[token],
        userName: token
      }
    }
  } else {
    ctx.body = { errno: Awesome! }
  }
})
router.post('/api/transfer'.ctx= > {
  const { body: { toUser, money }, token } = getReqData(ctx)
  if(! token) { ctx.body = {errno: Awesome!.errmsg: '30,000 yuan a month, come to work for dad -- Hang Zhou Ma'
    }
    return
  }
  db[toUser] += (+money)
  db[token] -= (+money)
  ctx.body = { errno: 0 }
})
app.use(koaBody())
app.use(router.routes(), router.allowedMethods())
app.use(koaStatic(path.resolve(__dirname, '.. /frond-end')))
app.listen(PORT, () = > {
  console.log(`the server is running in ${PORT}`)})Copy the code

Finally, verify a, is still the left and right browsers login quanquan and Zhangsan two users. And successfully transferred 20 yuan to quanquan account.

However, would it be a problem to “accidentally” open an indescribable website again?

We see hacker try to fake a money transfer request, fail, and receive an invitation from his dad to be a lucky child

We have rejected all CSRF attacks so far, and now we can eat hot pot and sing a song.

Afterword.

Thank you for your patience. We have received a series of safety work orders (XSS/CSRF, etc.) this year. As the team is more looking forward to CSRF content, I first wrote this article as an internal sharing. If you like I may also summarize other security work order resolution processes and vulnerability principles. Thank you.

Reference documentation

  • CSRF attacks
  • About CSRF, what is CSRF and how can it be protected?
  • Network Security -CSRF
  • Front-end Security Series 2: How do I Prevent CSRF Attacks?
  • The SameSite property of the Cookie
  • Historical version browser download
  • jwt.io