背景
1.我们业务系统有被第三方系统嵌套使用,通过Node端下发cookie来实现一键登录。
2.Chrome 80.0中将SameSite的默认值设为Lax。
解释
SameSite详解
SameSite Cookie,防止 CSRF 攻击
因为 HTTP 协议是无状态的,所以很久以前的网站是没有登录这个概念的,直到网景发明 cookie 以后,网站才开始利用 cookie 记录用户的登录状态。cookie 是个好东西,但它很不安全,其中一个原因是因为 cookie 最初被设计成了允许在第三方网站发起的请求中携带,CSRF 攻击就是利用了 cookie 的这一“弱点”,如果你不了解 CSRF,请移步别的地方学习一下再来。
当我们在浏览器中打开 a.com 站点下的一个网页后,这个页面后续可以发起其它的 HTTP 请求,根据请求附带的表现不同,这些请求可以分为两大类:
异步请求(不会改变当前页面,也不会打开新页面),比如通过 script、link、img、iframe 等标签发起的请求,还有通过各种发送 HTTP 请求的 DOM API(XHR,fetch,sendBeacon)发起的请求。
同步请求(可能改变当前页面,也可能打开新页面),比如通过对 a 的点击,对 form 的提交,还有改变 location.href,调用 window.open() 等方式产生的请求。
这些由当前页面发起的请求的 URL 不一定也是 a.com 上的,可能有 b.com 的,也可能有 c.com 的。我们把发送给 a.com 上的请求叫做第一方请求(first-party request),发送给 b.com 和 c.com 等的请求叫做第三方请求(third-party request),第三方请求和第一方请求一样,都会带上各自域名下的 cookie,所以就有了第一方 cookie(first-party cookie)和第三方 cookie(third-party cookie)的区别。上面提到的 CSRF 攻击,就是利用了第三方 cookie 。
SameSite=Strict:
严格模式,表明这个 cookie 在任何情况下都不可能作为第三方 cookie,绝无例外。比如说假如 b.com 设置了如下 cookie:
1 | Set-Cookie: foo=1; SameSite=Strict |
这种模式下你在 a.com 下发起的对 b.com 的任意请求中,foo 这个 cookie 都不会被包含在 Cookie 请求头中,但 bar 会
SameSite=Lax:
If the value is “Lax”, thecookie will be sent with same-site requests, and with “cross-site”top-level navigations, as described in Section 5.3.7.1.
上面规范已经说了,如果 SameSite 值是 Lax, 那么在发送同站请求的时候会带上 Cookie。那么跨站请求会不会带呢?上面说了这块规范在5.3.7.1中有写,我们看看这节内容相关的描述:
developers may set the “SameSite” attribute in a “Lax”enforcement mode that carves out an exception which sends same-sitecookies along with cross-site requests if and only if they are top-level navigations which use a “safe” (in the [RFC7231] sense) HTTPmethod.
宽松模式,比 Strict 放宽了点限制:假如这个请求是我上面总结的那种同步请求(改变了当前页面或者打开了新页面)且同时是个 GET 请求(因为从语义上说 GET 是读取操作,比 POST 更安全),则这个 cookie 可以作为第三方 cookie。比如说假如 b.com 设置了如下 cookie:
1 | Set-Cookie: foo=1; SameSite=Strict |
当用户从 a.com 点击链接进入 b.com 时,foo 这个 cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则 bar 也不会发送。
SameSite=None
cookie会像当前模式一样可以被第三方网站读取
注意当SameSite=None 必须同时添加 Secure, 而且 HTTPS是必须的。
1 | Set-cookie: key=value; SameSite=None; Secure |
影响
因为我们网站嵌在第三方系统内(iframe),对于我们是不可见的,客服反馈打开系统跳转到登录页,但是我们自己访问却很正常。
在我本机使用他们系统时也正常。
所以让客户打开 chrome://flags/#same-site-by-default-cookies
将sameSite by default cookies 设置为disable。一切正常了。
所以就定位到问题为 sameSite 默认被设置为lax导致iframe页面请求不携带原有cookie。
解决
系统内cookie为express从服务端写入。
express在4.17.0版本中 res.cookie()支持设置 sameSite为none
1 | res.cookie('name', 'tobi', { domain: '.example.com', path: '/admin', sameSite: 'none', secure: true }) |
注意该方式必须在https环境下进行!!
更新
之前在系统内写的cookie都已经是sameSite:’None’且secure为true了。
但是express-session 种的session key 没有做该操作。所以查阅express-session的文档, 在1.17.0版本中新增对SameSite=None的支持
1 | app.use(session({ |
设置结束以后cookie中的session key消失了。
删除secure cookie被种上,sameSite:None 也没问题。
但是根据上文,sameSite:None 必须与secure同时使用。
翻看express-session源码查找问题。
找到isSecure方法来进行判断是否可以为secure:true。
虽然我们系统时https的 但是 req.headers[‘x-forwarded-proto’]却是http。
因为我们系统使用nginx做反向代理了,如果是用 nginx 做了反向代理,那默认情况下是取不到的,因为 nginx 到 node 始终是 HTTP 的。
有一种方式是通过一个特定的 header 字段来处理,nginx 做反向代理的时候加上这个字段,node 里获取就可以了。
方案:
1 | location = /index.js { |
添加header: proxy_set_header X-Forwarded-Proto https;
解决问题。