被问出花了的 Cookie、CSRF

1,352 阅读4分钟

本文先解释 Cookie,对 Cookie 引起的 csrf\color{red}{csrf} 进行探索,并运用 Cookie 解决 csrf 漏洞。

Cookie 定义

我们知道 http 是无状态的,也就是说用户登录后进行其他操作时服务端无法知道用户是否已登录。

有机灵的小伙伴很快想到,把用户登录信息存起来后面的接口都带上不就可以了?但 Cookie 出现前浏览器端还没有本地存储方法,JS 中在强的变量也抵不过浏览器的一次刷新。就这样Cookie\color{red}{ Cookie } 亮相了。

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于保持用户的登录状态。Cookie使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

Cookie主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

cookie 在性质上是绑定在特定域名下的。当设定了一个cookie后,再给创建它的域名发请求时都会包含这个cookie。这个限制确保了存在cookie中的信息只能让批准的接受者访问。而无法被其他域名访问。

Cookie 属性

ctx.cookies.set(name, value, [options]) 通过 options 设置 cookie name 的 value :

  • maxAge 一个数字表示从 Date.now() 得到的毫秒数
  • signed cookie 签名值 expires cookie 过期的Date
  • path cookie 路径, 默认是’/’ domain cookie 域名 secure 安全 cookie
  • httpOnly 加以限制,使 Cookie 不能被 JavaScript 脚本访问
  • overwrite 一个布尔值,表示是否覆盖以前设置的同名的
  • Secure 仅在 HTTPS 安全通信时才会发送 Cookie

csrf 实战

csrf 常常用这样一个场景举例,有一个银行它的网址是 localhost:8000, 用户登录后不小心打开钓鱼网站,并被诱导发生转账。

我不是很相信 钓鱼网址 可以携带银行网址的 cookie,所以实验是不可避免的了。

├── bad-end // 钓鱼网站
├──── app.js //服务器
├──── public //静态文件夹
├────── index.html
├── front // 银行网站
├──── app.js //服务器
├──── public //静态文件夹
├────── index.html //首页
├────── login.html //登录页

  1. 建立银行端服务 (front/app.js)
const Koa = require('koa');
const route = require("koa-route")
const fs = require("fs");
const path = require("path");
const bodyParser = require('koa-bodyparser');
const app = new Koa();
// 存储用户信息
let users = [{
  name: "xiaoMing",
  password: "123456",
  money: 10000
},{
  name: "badMan",
  password: "123456",
  money: 0
}]

let sessions = {}

app.use(bodyParser())

//转账接口
app.use(route.post('/api/transfer', ctx => {
  let body = ctx.request.body;
  let fromName = JSON.parse(ctx.cookies.get("key")).name;
  if(!sessions[fromName]) return;
  let toUser = users.find((item) => {
    return item.name === body.toName;
  })
  let fromUser = users.find((item) => {
    return item.name === fromName;
  })

  if(toUser && fromUser && body.price){
    let price = Number(body.price)
    toUser.money +=  price;
    fromUser.money -= price;
    console.log(JSON.stringify(users))
  }
}));

// 静态文件服务
app.use(route.get('/public/*', ctx => {
  ctx.body = getPublicFile(`.${ctx.request.url}`)
}));

// 登录接口
app.use(route.post('/api/login', ctx => {
  let body = ctx.request.body;
  let find = users.find((item) => {
    return item.name === body.name && item.password === body.password;
  })
  if(find){
    //设置 session
    sessions[body.name] = body.name;
    //设置cookie
    ctx.cookies.set("key", JSON.stringify(find) )
    ctx.body = getPublicFile(`./public/index.html`)
  }
}));

function getPublicFile(url){
  return fs.readFileSync( path.resolve(__dirname, url),{encoding: 'utf8' } );
}

app.listen(8000);
  1. 创建登录页面(front/public/login.html)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Hello Cookie!</h1>
    <form action="/api/login" method="post">
        <div>
            <input type="text" name="name"/>
        </div>
        <div>
            <input type="text" name="password"/>
        </div>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>
  1. 再创建一个简单的首页(front/public/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Bank</h1>
</body>
</html>
  1. 输入页面地址 http://localhost:8000/public/login.html,填写用户名密码登录并提交
  2. 登录成功并跳转到首页拿到服务端写入的 cookie
  3. 创建钓鱼网站服务器(bad-end/app.js)
const Koa = require('koa');
const route = require("koa-route")
const fs = require("fs");
const path = require("path");
const app = new Koa();

app.use(bodyParser())
app.use(route.get('/public/*', ctx => {
  ctx.body = getPublicFile(`.${ctx.request.url}`)
}));

function getPublicFile(url){
  return fs.readFileSync( path.resolve(__dirname, url),{encoding: 'utf8' } );
}

app.listen(3000);
  1. 创建钓鱼页面 (bad-end/public/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="http://localhost:8000/api/transfer" method="post">
        <div style="display: none;">
            <input type="text" name="toName" value="badMan" id="">
            <input type="text" name="price" value="1000" id="">    
        </div>
        <input type="submit" value="点我">
    </form>
</body>
</html>
  1. 当我们在同一浏览器下,打开 localhost:3000

点击提交发现 transfer 接口收到小明的Cookie,并以小明的身份给黑客转了 1000 元。也就印证了 钓鱼网址 访问银行接口时真的带了 银行登录 的 Cookie。

  1. 浏览器端某个Cookie的domain字段等于aaa.www.com或者www.com
  2. 都是http或者https,或者不同的情况下Secure属性为false
  3. 要发送请求的路径,即上面的xxxxx跟浏览器端Cookie的path属性必须一致,或者是浏览器端Cookie的path的子目录,比如浏览器端Cookie的path为/test,那么xxxxxxx必须为/test或者/test/xxxx等子目录才可以

上面3个条件必须同时满足,否则该Post请求就不能自动带上浏览器端已存在的Cookie

基于 Cookie 的 csrf 防御

1. 验证 HTTP Referer 字段

通过 http 请求头 referer 字段,通过 referer 可访问白名单过滤掉非法的 Cookie 携带者。但这个方法不是万无一失的,有些浏览器的 Referer 有被修改的漏洞。

2. SameSite 属性

Cookie 的 SameSite\color{red}{SameSite} 属性用来限制第三方 Cookie,从而减少安全风险。
它可以设置三个值:

  1. Strict 跨站点时,任何情况下都不会发送 Cookie
  2. Lax 大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外
  3. None 网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
SameSite=None; Secure
SameSite=Strict

其他csrf 防御

1. 浏览器端使用token

通过 jwt\color{red}{jwt} 生成 token ,使用 stroage 存储 token,并携带进后续的请求头中。实现状态识别。

参考

  • Cookie 的 SameSite 属性
  • MDN