express如何防范跨站请求伪造(csrf)

525 阅读3分钟

示例代码仓库地址

github.com/ishanyang/b…

示例代码是通过nodejs express框架完成

模拟实现csrf(文件目录 csrf-example)

创建app 用于提供表单提交服务

mkdir app
cd app
npm init -y
npm install express pug --save
mkdir views
touch app.js views/layout.pug views/list.pug views/post.pug

app.js

const path = require('path');
const express = require('express');
const pug = require('pug');

const app = express();
let list = [
  {title: '文章一'},
  {title: '文章二'},
];

app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', function(req, res, next) {
  res.render('list', {
    title: '文章列表',
    list
  })
});

app.get('/post', function(req, res, next) {
  res.render('post', {
    title: '新增文章'
  });
});

app.post('/post', function(req, res, next) {
  const title = req.body.title
  if(title) {
    list.push({title})
  }
  return res.redirect('/');
});

app.get('*', function (req, res) {
  res.send('hello world!')
})
app.listen(4001, () => {
  console.log('http://localhost:4001/')
})

views/layout.pug

doctype html
html
  head
    title= title
  body
    block content

views/list.pug

extends layout

block content
  a(href="/post") 新增文章
  each article in list
    h1 #{article.title}

views/post.pug

extends layout

block content
  form(action="/post" method="POST")
    input(type="text" name="title" required placeholder="请输入文章标题")
    button(type="submit") 提交

执行 node app.js 访问http://localhost:4001/ 效果如下

1.png

创建模拟攻击的服务

touch mockCsrf.js views/mockCsrf.pug

mockCsrf.js

const path = require('path');
const express = require('express');
const pug = require('pug');

const app = express();

app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

app.get('*', function (req, res) {
  res.render('mockCsrf', {title: 'mock csrf'});
})

app.listen(4002, () => {
  console.log('mock csrf http://localhost:4002/')
})

views/mockCsrf.pug

extends layout

block content
  h1 mock csrf
  form#mock-form(action="http://localhost:4001/post" method="POST")
    input(type="text" name="title" required placeholder="请输入文章标题" value="这是一篇通过跨站请求伪造发布的文章")
    button(type="submit") 提交
  script.
    document.getElementById("mock-form").submit();

执行 node mockCsrf.js 访问http://localhost:4002/ 效果如下

2.png

通过检查Referer字段防范csrf(@authentication/csrf-protection)

在csrf-example基础上,安装@authentication/csrf-protection

npm install @authentication/csrf-protection --save

app.js

const path = require('path');
const express = require('express');
const pug = require('pug');
const csrfProtection = require('@authentication/csrf-protection')

const app = express();
let list = [
  {title: '文章一'},
  {title: '文章二'},
];

app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

app.use(csrfProtection());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', function(req, res, next) {
  res.render('list', {
    title: '文章列表',
    list
  })
});

app.get('/post', function(req, res, next) {
  res.render('post', {
    title: '新增文章'
  });
});

app.post('/post', function(req, res, next) {
  const title = req.body.title
  if(title) {
    list.push({title})
  }
  return res.redirect('/');
});

app.get('*', function (req, res) {
  res.send('hello world!')
})
app.listen(4001, () => {
  console.log('http://localhost:4001/')
})

重启服务

npm start
npm run start-mock-csrf

再次访问csrf攻击服务 http://localhost:4002/ 效果如下:

csrf-referer-exmaple.png

至此通过检查Referer字段的防范csrf实现完成。

通过令牌模式防范csrf

在csrf-example基础上,安装依赖

npm i csurf csrf cookie-parser --save

在实现令牌同步模式之前,先了解下package csrf,因为express中间件csurf使用了此库。应用至少保证每个用户的csrfSecret不同

const Tokens = require('csrf');

const tokens = new Tokens();
// 创建密钥,异步生成的方式:tokens.secret(callback)
const csrfSecret = tokens.secretSync();
// 创建令牌
const some_user_token = tokens.create(csrfSecret);

// 验证令牌
if (!tokens.verify(csrfSecret, some_user_token)) {
  throw new Error('invalid token!')
} else {
  console.log('验证通过');
}

修改views/post.pug内容如下

extends layout

block content
  form(action="/post" method="POST")
    input(type="hidden" name="_csrf" value= csrfToken)
    input(type="text" name="title" required placeholder="请输入文章标题")
    button(type="submit") 提交

修改app.js内容如下

const path = require('path');
const express = require('express');
const pug = require('pug');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');

const csrfProtection = csrf({ cookie: true })

const app = express();
let list = [
  {title: '文章一'},
  {title: '文章二'},
];

app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

app.use(cookieParser())
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', function(req, res, next) {
  res.render('list', {
    title: '文章列表',
    list
  })
});

app.get('/post', csrfProtection, function(req, res, next) {
  res.render('post', {
    title: '新增文章',
    csrfToken: req.csrfToken()
  });
});

app.post('/post', csrfProtection, function(req, res, next) {
  const title = req.body.title
  if(title) {
    list.push({title})
  }
  return res.redirect('/');
});

app.get('*', function (req, res) {
  res.send('hello world!')
})
app.listen(4001, () => {
  console.log('http://localhost:4001/')
})

再次运行 node app.js 和 node mockCsrf.js

访问 http://localhost:4001 可以正常添加文章

但是再访问http://localhost:4002 试图跨站伪造攻击时的结果如下

ForbiddenError: invalid csrf token

其它比如ajax,或者单页APP传token的用法等请参考:github.com/expressjs/c…

原文链接

www.hijs.ren/article/blo…