nswbmw/N-Blog 学习-5-登出 Logout, Session 和 Flash 的再次研究, cookie.id 和 cookieSecret 的研究

登出响应 logout 的实现

index.js

app.post('/login') :

app.post('/login', function (req, res) {
//生成密码的 md5 值
var md5 = crypto.createHash('md5'),
password = md5.update(req.body.password).digest('hex');
//检查用户是否存在
User.get(req.body.name, function (err, user) {
if (!user) {
req.flash('error', '用户不存在!');
//@d4rkb1ue 在这里return res.() 其实并不是在return response.
//res.redirect 肯定返回了一个无所谓的值。其实就是
//res.redirect();return null;
return res.redirect('/login');//用户不存在则跳转到登录页
}
//检查密码是否一致
if (user.password != password) {
req.flash('error', '密码错误!');
return res.redirect('/login');//密码错误则跳转到登录页
}
//用户名密码都匹配后,将用户信息存入 session
req.session.user = user;
req.flash('success', '登陆成功!');
res.redirect('/');//登陆成功后跳转到主页
});
});

对这段代码的研究

req.session.user = user;
req.flash('success','welcome!'+user.name);
res.redirect('/');

首先req.session/flash的操作都是本地的。即去修改本地数据库里面session信息。
之后在res.redirect是在给予用户反馈。但是当然的,这次发送其实是告诉用户去 GET /而不是直接送给用户/的内容(可以在包拦截中看到实际上是给了一个302,临时转址)。然后之后用户/的请求,自带了cookie.id,因此服务器返回的是符合这个id的内容,包括去属于这个id的session里拿到flash,给用户。之后删除flash。

app.get('/login') :

app.get('/login', function (req, res) {
res.render('login', {
title: '登录',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()});
});

不加这段,在不登陆状态访问 /reg 就会报错找不到 user

app.get('/logout'):

app.get('/logout', function (req, res) {
req.session.user = null;
req.flash('success', '登出成功!');
res.redirect('/');//登出成功后跳转到主页
});

req.session.user 中把 user 信息赋值为空。就等于这个用户的cookie.id来数据库中找的时候,发现没有user。于是就返回未登录状态的界面。

Tips:用db.sessions.remove({})可以删除所有的session。

第一次研究登陆的过程,我觉得漏洞很多

  1. 首先,密码原文是明文传输的。只是在server端不保存原文而已。
  2. server判定用户是通过cookie id,那么如果有人伪造id呢?
  3. md5保存密码没有问题,一定程度上减少了被拖库的威胁。但是session数据库被拖库了以后,对方不就知道这个用户的cookie id了吗,继而就可以仿造cookie了。
app.use(session({
secret: settings.cookieSecret
key: settings.db,//cookie name
...
})}));

在session中,我们添加了一个 cookieSecret 我猜测这个东西对我们的 cookie 做了加密。

看下cookie(从chrome的开发者工具的 Resources->Cookies 可以看到 Cookies):

Name: blog
Value: s%3ATKcqS6XfuhObSk2vFY1UuNI_ehtpn8MM.b8NAkCxLu85FB2OiusE8FftGMKS2VEOJppODD2eD5%2FU
Domain: node
Path: /
Exp. time: 2016-05-01T09:23:32.673Z
Size: 86
HTTP: ✓ true

我在同一个浏览器中重新注册了一个账号’r’,看看新的Cookie:

blog    
s%3ATKcqS6XfuhObSk2vFY1UuNI_ehtpn8MM.b8NAkCxLu85FB2OiusE8FftGMKS2VEOJppODD2eD5%2FU
node
/
2016-05-02T11:23:50.520Z
86

也就是说,value没变!只是Expire Time变了。

再来看看数据库的存储:

> db.sessions.find()
{ "_id" : "TKcqS6XfuhObSk2vFY1UuNI_ehtpn8MM", "session" : "{\"cookie\":{\"originalMaxAge\":2591999997,\"expires\":\"2016-04-30T21:36:25.235Z\",\"httpOnly\":true,\"path\":\"/\"},\"flash\":{},\"user\":{\"name\":\"r\",\"password\":\"c4ca4238a0b923820dcc509a6f75849b\",\"email\":\"[email protected]\"}}", "expires" : ISODate("2016-04-30T21:36:25.235Z") }

可以看到 user:name:r 就是刚刚申请的那个。值得注意的是,expire time 没变!看来client 和 server各自维护自己的expire时间。

注意_id正是Client:Cookie存储的s%3ATKcqS6XfuhObSk2vFY1UuNI_ehtpn8MM.只是多了个s%3A前缀罢了。那么我说的这种cookie欺骗是有可能的。

新注册一个Cookie

再看一个cookie, 使用chrome隐身模式,注册一个 ‘niming’ 的user:
得到Cookie:

blog    s%3ADLGJsEugpRcnMGtH37frp_uQQ2B-RXUp.iL5i3ATdzjoABjwAHZF554jkpfFtY%2B%2FICrOFzADEH2A    node    /   2016-05-02T11:43:58.855Z    88  ✓

>db.sessions.find():

{ "_id" : "DLGJsEugpRcnMGtH37frp_uQQ2B-RXUp", "session" : "{\"cookie\":{\"originalMaxAge\":2592000000,\"expires\":\"2016-04-30T21:56:26.531Z\",\"httpOnly\":true,\"path\":\"/\"},\"flash\":{},\"user\":{\"name\":\"niming\",\"password\":\"c4ca4238a0b923820dcc509a6f75849b\",\"email\":\"[email protected]\"}}", "expires" : ISODate("2016-04-30T21:56:26.531Z") }

//TODO: expire time 到底怎么回事。。不是很关键。日后再说。
问题是,依然一样。前缀也依然是s%3A,后面一样。

更改 cookieSecret = myblog?

我更改下 cookieSecret = myblog? 看看是不是影响前缀。

  1. 首先修改cookieSecret后,用户端的原cookie失效了。并且申领了一个新的cookie:

    s%3A82YXnbBiTqEbnFWVtkyaQPcZ0yxAD1Oz.97K96xtAF186ejUJMWCMcGg7Hq9J5w017YiqCzR8RTg
    
  1. 注册一个新用户 “new”. Client.Cookie.Value 并没有发生改变。
  2. 看看 Server.db.Session.Cookie:

    { "_id" : "82YXnbBiTqEbnFWVtkyaQPcZ0yxAD1Oz", "session" : "{\"cookie\":{\"originalMaxAge\":2592000000,\"expires\":\"2016-04-30T22:07:14.027Z\",\"httpOnly\":true,\"path\":\"/\"},\"flash\":{},\"user\":{\"name\":\"new\",\"password\":\"c4ca4238a0b923820dcc509a6f75849b\",\"email\":\"[email protected]\"}}", "expires" : ISODate("2016-04-30T22:07:14.027Z") }
    
  1. 可以看到前缀没有变化。但是是不是Client.Cookie.Value的后半部分.97K96xtAF186ejUJMWCMcGg7Hq9J5w017YiqCzR8RTg有用呢?是不是Client在Request的时候传入了后者。而后者是由Server之前对前半部分用 种子为 cookieSecret 进行加密的结果,传给Client作为凭证。
  2. 尝试注释掉 secret: settings.cookieSecret 这一行。结果是报错,Error: secret option required for sessions。把setting.js里的secret设为空。也报一样的错误。

  3. 后者就是Server给的!可以去chrome的network点击’node’(也就是索取的host)页面,就可以看到response,request,和里面的cookie了!可以看到的确server提供了value的后半段,而且每次发送的时候也发送全部的。

使用Chrome Extension Tamper Chrome

安装Tamper Chrome

两部分,分为 Application 和 Extension:

Application

Extension

都要安装。Extension负责拦截,App负责显示。

启动

在地址栏输入 chrome://apps 进入应用启动界面。开启Tamper App.
在开发者工具 Element Console ... 这一行点击最后面的 >> 开启Tamper Ext.之后可以选择拦截什么东西。第一个应该是总开关。后面是拦截的所有选项。
开启 block / reroute requestsrequest headers 可选 ignore 一些 request。不必去拦截这些。

当注册成功返回主页的时候,register 选项不可见。证明已经是已登陆状态。
再次刷新依然是已登陆状态的。

再次刷新的时候,拦截,并且修改cookie的后半段。就是.之后的部分。哈哈!的确是退出登陆状态了。而且server的返回也再分派给client一个新的cookie。

结论

如果把cookie.value分为 x.y 的话。这2部分都是由server生成的。x存储在数据库中。y存储在用户cookie中。在用户发送request的时候,同时发送x.y。由server判断 加密(x,cookieSecret) === y,如果不等于,就认为用户有误。不予登录。

因此,如果要达成欺骗,就需要同时获取到 x.y,单单更改x是不行的。