赵走x博客
网站访问量:151929
首页
书籍
软件
工具
古诗词
搜索
登录
Flask 实战:41、电子邮件进阶实践
Flask 实战:40、使用事务邮件服务SendGird
Flask 实战:39、使用Flask-Mail发送电子邮件
Flask 实战:38、数据库进阶实践
Flask 实战:37、更新数据库表
Flask 实战:36、定义关系
Flask 实战:35、在视图函数里操作数据库
Flask 实战:34、数据库操作:CRUD
Flask 实战:33、使用Flask-SQLAlchemy管理数据库
Flask 实战:32、ORM魔法
Flask 实战:31、数据库的分类
Flask 实战:30、单个页面多个表单
Flask 实战:29、单个表单多个提交按钮
Flask 实战:28、使用Flask-CKEditor集成富文本编辑器
Flask 实战:27、多文件上传
Flask 实战:26、文件上传
Flask 实战:25、自定义验证器
Flask 实战:24、使用宏渲染表单
Flask 实战:23、设置错误消息语言
Flask 实战:22、处理表单数据
Flask 实战:21、使用Flask-WTF处理表单
Flask 实战:20、HTML表单
Flask 实战:19、模板进阶实践
Flask 实战:18、模板结构组织
Flask 实战:17、模板辅助工具
Flask 实战:16、模板基本用法
Flask 实战:15、HTTP进阶实践
Flask 实战:14、Flask上下文
Flask 实战:13、HTTP响应
Flask 实战:12、HTTP请求
Flask 实战:11、请求响应循环
Flask 实战:10、Flask与MVC架构
Flask 实战:9、模板与静态文件
Flask 实战:8、Flask命令
Flask 实战:7、URL与端点
Flask 实战:6、项目配置
Flask 实战:5、Flask扩展
Flask 实战:4、Python Shell
Flask 实战:3、启动开发服务器
Flask 实战:2、Hello,Flask!
Flask 实战:1、初识Flask
Flask 实战:15、HTTP进阶实践
资源编号:75851
Python Web
Flask Web开发实战:入门、进阶与原理解析
热度:76
在本书的第一部分,从本章开始,每一章的最后都会包含一个“进阶实践”部分,其中介绍的内容我们将会在第二部分的程序实例中使用到。在这一节,我们会接触到一些关于HTTP的进阶内容。
在本书的第一部分,从本章开始,每一章的最后都会包含一个“进阶实践”部分,其中介绍的内容我们将会在第二部分的程序实例中使用到。在这一节,我们会接触到一些关于HTTP的进阶内容。 # 1、重定向回上一个页面 在前面的示例程序中,我们使用redirect()函数生成重定向响应。比如,在login视图中,登入用户后我们将用户重定向到/hello页面。在复杂的应用场景下,我们需要在用户访问某个URL后重定向到上一个页面。最常见的情况是,用户单击某个需要登录才能访问的链接,这时程序会重定向到登录页面,当用户登录后合理的行为是重定向到用户登录前浏览的页面,以便用户执行未完成的操作,而不是直接重定向到主页。在示例程序中,我们创建了两个视图函数foo和bar,分别显示一个Foo页面和一个Bar页面,如下所示: ``` @app.route('/foo') def foo(): return '
Foo page
Do something
' % url_for('do_something') @app.route('/bar') def bar(): return '
Bar page
Do something
' % url_for('do_something') ``` 在这两个页面中,我们都添加了一个指向do_something视图的链接。这个do_something视图如下所示: ``` @app.route('/do_something') def do_something(): # do something return redirect(url_for('hello')) ``` 我们希望这个视图在执行完相关操作后能够重定向回上一个页面,而不是固定的/hello页面。也就是说,如果在Foo页面上单击链接,我们希望被重定向回Foo页面;如果在Bar页面上单击链接,我们则希望返回到Bar页面。这一节我们会借助这个例子来介绍这一功能的实现。 ### 1.获取上一个页面的URL 要重定向回上一个页面,最关键的是获取上一个页面的URL。上一个页面的URL一般可以通过两种方式获取: #### (1)HTTP referer HTTP referer(起源为referrer在HTTP规范中的错误拼写)是一个用来记录请求发源地址的HTTP首部字段(HTTP_REFERER),即访问来源。当用户在某个站点单击链接,浏览器向新链接所在的服务器发起请求,请求的数据中包含的HTTP_REFERER字段记录了用户所在的原站点URL。 这个值通常会用来追踪用户,比如记录用户进入程序的外部站点,以此来更有针对性地进行营销。在Flask中,referer的值可以通过请求对象的referrer属性获取,即request.referrer(正确拼写形式)。现在,do_something视图的返回值可以这样编写: ``` return redirect(request.referrer) ``` 但是在很多种情况下,referrer字段会是空值,比如用户在浏览器的地址栏输入URL,或是用户出于保护隐私的考虑使用了防火墙软件或使用浏览器设置自动清除或修改了referrer字段。我们需要添加一个备选项: ``` return redirect(request.referrer or url_for('hello')) ``` #### (2)查询参数 除了自动从referrer获取,另一种更常见的方式是在URL中手动加入包含当前页面URL的查询参数,这个查询参数一般命名为next。比如,下面在foo和bar视图的返回值中的URL后添加next参数: ``` from flask import request @app.route('/foo') def foo(): return '
Foo page
Do something and redirect
' % url_for('do_something', next=request.full_path) @app.route('/bar') def bar(): return '
Bar page
Do something and redirect
' % url_for('do_something', next=request.full_path) ``` 在程序内部只需要使用相对URL,所以这里使用request.full_path获取当前页面的完整路径。在do_something视图中,我们获取这个next值,然后重定向到对应的路径: ``` return redirect(request.args.get('next')) ``` 用户在浏览器的地址栏直接访问时可以轻易地修改查询参数,为了避免next参数为空的情况,我们也要添加备选项,如果为空就重定向到hello视图: ``` return redirect(request.args.get('next', url_for('hello'))) ``` 为了覆盖更全面,我们可以将这两种方式搭配起来一起使用:首先获取next参数,如果为空就尝试获取referer,如果仍然为空,那么就重定向到默认的hello视图。因为在不同视图执行这部分操作的代码完全相同,我们可以创建一个通用的redirect_back()函数,如代码清单2-8所示。 代码清单2-8 http/app.py:重定向回上一个页面 ``` def redirect_back(default='hello', **kwargs): for target in request.args.get('next'), request.referrer: if target: return redirect(target) return redirect(url_for(default, **kwargs)) ``` 通过设置默认值,我们可以在referer和next为空的情况下重定向到默认的视图。在do_something视图中使用这个函数的示例如下所示: ``` @app.route('/do_something_and_redirect') def do_something(): # do something return redirect_back() ``` ### 2.对URL进行安全验证 虽然我们已经实现了重定向回上一个页面的功能,但安全问题不容小觑,鉴于referer和next容易被篡改的特性,如果我们不对这些值进行验证,则会形成开放重定向(Open Redirect)漏洞。 以URL中的next参数为例,next变量以查询字符串的方式写在URL里,因此任何人都可以发给某个用户一个包含next变量指向任何站点的链接。举个简单的例子,如果你访问下面的URL: ``` http://localhost:5000/do-something?next=http://helloflask.com ``` 程序会被重定向到[http://helloflask.com](http://helloflask.com) 。也就是说,如果我们不验证next变量指向的URL地址是否属于我们的应用内,那么程序很容易就会被重定向到外部地址。你也许还不明白这其中会有什么危险,下面假设的情况也许会给你一个清晰的认识: 假设我们的应用是一个银行业务系统(下面简称网站A),某个攻击者模仿我们的网站外观做了一个几乎一模一样的网站(下面简称网站B)。接着,攻击者伪造了一封电子邮件,告诉用户网站A账户信息需要更新,然后向用户提供一个指向网站A登录页面的链接,但链接中包含一个重定向到网站B的next变量,比如:[http://exampleA.com/login?next=http://maliciousB.com](http://exampleA.com/login?next=http://maliciousB.com) 。当用户在A网站登录后,如果A网站重定向到next对应的URL,那么就会导致重定向到攻击者编写的B网站。因为B网站完全模仿A网站的外观,攻击者就可以在重定向后的B网站诱导用户输入敏感信息,比如银行卡号及密码。 确保URL安全的关键就是判断URL是否属于程序内部,在代码清单2-9中,我们创建了一个URL验证函数is_safe_url(),用来验证next变量值是否属于程序内部URL。 代码清单2-9 http/app.py:验证URL安全性 ``` from urllib.parse import urlparse,urljoin def is_safe_url(target): # host_url:http://www.baidu.com/ ref_url=urlparse(request.host_url) test_url=urlparse(urljoin(request.host_url,target)) return test_url.scheme in ('http','https') and ref_url.netloc==test_url.netloc ``` 这个函数接收目标URL作为参数,并通过request.host_url获取程序内的主机URL,然后使用urljoin()函数将目标URL转换为绝对URL。接着,分别使用urlparse模块提供的urlparse()函数解析两个URL,最后对目标URL的URL模式和主机地址进行验证,确保只有属于程序内部的URL才会被返回。在执行重定向回上一个页面的redirect_back()函数中,我们使用is_safe_url()验证next和referer的值: ``` def redirect_back(default='hello', **kwargs): for target in request.args.get('next'), request.referrer: if not target: continue if is_safe_url(target): return redirect(target) return redirect(url_for(default, **kwargs)) ``` 关于开放重定向漏洞的更多信息可以访问[https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet](https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet) 了解。 # 2、使用AJAX技术发送异步请求 在传统的Web应用中,程序的操作都是基于请求响应循环来实现的。每当页面状态需要变动,或是需要更新数据时,都伴随着一个发向服务器的请求。当服务器返回响应时,整个页面会重载,并渲染新页面。 这种模式会带来一些问题。首先,频繁更新页面会牺牲性能,浪费服务器资源,同时降低用户体验。另外,对于一些操作性很强的程序来说,重载页面会显得很不合理。比如我们做了一个Web计算器程序,所有的按钮和显示屏幕都很逼真,但当我们单击“等于”按钮时,要等到页面重新加载后才在显示屏幕上看到结果,这显然会严重影响用户体验。我们这一节要学习的AJAX技术可以完美地解决这些问题。 ### 1.认识AJAX AJAX指异步Javascript和XML(Asynchronous JavaScript And XML),它不是编程语言或通信协议,而是一系列技术的组合体。简单来说,AJAX基于XMLHttpRequest([https://xhr.spec.whatwg.org/](https://xhr.spec.whatwg.org/) )让我们可以在不重载页面的情况下和服务器进行数据交换。加上JavaScript和DOM(Document Object Model,文档对象模型),我们就可以在接收到响应数据后局部更新页面。而XML指的则是数据的交互格式,也可以是纯文本(Plain Text)、HTML或JSON。顺便说一句,XMLHttpRequest不仅支持HTTP协议,还支持FILE和FTP协议。 > 提示 AJAX也常被拼作Ajax,但是为了和古希腊神话里的英雄Ajax区分开来,在本书中将使用全大写形式,即AJAX。 在Web程序中,很多加载数据的操作都可以在客户端使用AJAX实现。比如,当用户鼠标向下滚动到底部时在后台发送请求获取数据,然后插入文章;再比如,用户提交表单创建新的待办事项时,在后台将数据发送到服务器端,保存后将新的条目直接插入到页面上。 在这种模式下,我们可以在客户端实现大部分页面逻辑,而服务器端则主要负责处理数据。这样可以避免每次请求都渲染整个页面,这不仅增强了用户体验,也降低了服务器的负载。AJAX让Web程序也可以像桌面程序那样获得更流畅的反应和动态效果。总而言之,AJAX让Web程序更像是程序,而非一堆使用链接和按钮连接起来的网页资源。 以删除某个资源为例,在普通的程序中流程如下: * 1)当“删除”按钮被单击时会发送一个请求,页面变空白,在接收到响应前无法进行其他操作。 * 2)服务器端接收请求,执行删除操作,返回包含整个页面的响应。 * 3)客户端接收到响应,重载整个页面。 使用AJAX技术时的流程如下: * 1)当单击“删除”按钮时,客户端在后台发送一个异步请求,页面不变,在接收响应前可以进行其他操作。 * 2)服务器端接收请求后执行删除操作,返回提示消息或是无内容的204响应。 * 3)客户端接收到响应,使用JavaScript更新页面,移除资源对应的页面元素。 ### 2.使用jQuery发送AJAX请求 jQuery是流行的JavaScript库,它包装了JavaScript,让我们通过更简单的方式编写JavaScript代码。对于AJAX,它提供了多个相关的方法,使用它可以很方便地实现AJAX操作。更重要的是,jQuery处理了不同浏览器的AJAX兼容问题,我们只需要编写一套代码,就可以在所有主流的浏览器正常运行。 ** 提示 使用jQuery实现AJAX并不是必须的,你可以选择使用原生的XMLHttpRequest、其他JavaScript框架内置的AJAX接口,或是使用更新的Fetch API([https://fetch.spec.whatwg.org/](https://fetch.spec.whatwg.org/) )来发送异步请求。** 在示例程序中,我们将使用全局jQuery函数ajax()发送AJAX请求。ajax()函数是底层函数,有丰富的自定义配置,支持的主要参数如表2-13所示。 > 附注 完整的可用配置参数列表可以在这里看到:[http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings](http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings) 。  表2-13 ajax()函数支持的参数 jQuery还提供了其他快捷方法(shorthand method):用于发送GET请求的get()方法和用于发送POST请求的post()方法,还有直接用于获取json数据的getjson()以及获取脚本的getscript()方法。这些方法都是基于ajax()方法实现的。在这里,为了便于理解,使用了底层的ajax方法。jQuery中和AJAX相关的方法及其具体用法可以在这里看到:[http://api.jquery.com/category/ajax/](http://api.jquery.com/category/ajax/) 。 ### 3.返回“局部数据” 对于处理AJAX请求的视图函数来说,我们不会返回完整的HTML响应,这时一般会返回局部数据,常见的三种类型如下所示: #### 1.纯文本或局部HTML模板 纯文本可以在JavaScript用来直接替换页面中的文本值,而局部HTML则可以直接到插入页面中,比如返回评论列表: ``` @app.route('/comments/
') def get_comments(post_id): ... return render_template('comments.html') ``` #### 2.JSON数据 JSON数据可以在JavaScript中直接操作: ``` @app.route('/profile/
') def get_profile(user_id): ... return jsonify(username=username, bio=bio) ``` 在jQuery中的ajax()方法的success回调中,响应主体中的JSON字符串会被解析为JSON对象,我们可以直接获取并进行操作。 #### 3.空值 有些时候,程序中的某些接收AJAX请求的视图并不需要返回数据给客户端,比如用来删除文章的视图。这时我们可以直接返回空值,并将状态码指定为204(表示无内容),比如: ``` @app.route('/post/delete/
', methods=['DELETE']) def delete_post(post_id): ... return '', 204 ``` ### 4.异步加载长文章示例 在示例程序的对应页面中,我们将显示一篇很长的虚拟文章,文章正文下方有一个“加载更多”按钮,当加载按钮被单击时,会发送一个AJAX请求获取文章的更多内容并直接动态插入到文章下方。用来显示虚拟文章的show_post视图如代码清单2-10所示。 代码清单2-10 http/app.py:显示虚拟文章 ``` from jinja2.utils import generate_lorem_ipsum @app.route('/post') def show_post(): post_body = generate_lorem_ipsum(n=2) # 生成两段随机文本 return '''
A very long post
%s
Load More
''' % post_body ``` 文章的随机正文通过Jinja2提供的generate_lorem_ipsum()函数生成,n参数用来指定段落的数量,默认为5,它会返回由随机字符组成的虚拟文章。文章下面添加了一个“加载更多”按钮。按钮下面是两个代码块,第一个script从CDN加载jQuery资源。 在第二个script标签中,我们在代码的最外层创建了一个$(function(){...})函数,这个函数是常见的$(document).ready(function(){...})函数的简写形式。这个函数用来在页面DOM加载完毕后执行代码,类似传统JavaScript中的window.onload方法,所以我们通常会将代码包装在这个函数中。美元符号是jQuery的简写,我们通过它来调用jQuery提供的多个方法,所以$.ajax()等同于jQuery.ajax()。 在$(function(){...})中,$('#load')被称为选择器,我们在括号中传入目标元素的id、class或是其他属性来定位到对应的元素,将其创建为jQuery对象。我们传入了“加载更多”按钮的id值以定位到加载按钮。在这个选择器上,我们附加了.click(function(){...}),这会为加载按钮注册一个单击事件处理函数,当加载按钮被单击时就会执行单击事件回调函数。在这个回调函数中,我们使用$.ajax()方法发送一个AJAX请求到服务器,通过url将目标URL设为“/more”,通过type参数将请求的类型设为GET。当请求成功处理并返回2XX响应时(另外还包括304响应),会触发success回调函数。success回调函数接收的第一个参数为服务器端返回的响应主体,在这个回调函数中,我们在文章正文(通过$('.body')选择)底部使用append()方法插入返回的data数据。 由于篇幅所限,我们不会深入介绍JavaScript或jQuery,你可以阅读其他书籍来学习更多内容。 处理/more的视图函数会返回随机文章正文,如下所示: ``` @app.route('/more') def load_post(): return generate_lorem_ipsum(n=1) ``` 如果你启动了示例程序,那么访问[http://localhost:5000/post](http://localhost:5000/post) 可以看到文章页面,当你单击文章下的“Load More”按钮时,浏览器就会在后台发送一个GET请求到/more,这个视图返回的随机字符会被动态插入到文章下方。 在出版业和设计业,lorem ipsum指一段常用的无意义的填充文字。以lorem ipsum开头的这段填充文本是抽取哲学著作《On the ends of good and evil》中的文段,并对单词进行删改调换而来。 # 3、HTTP服务器端推送 不论是传统的HTTP请求-响应式的通信模式,还是异步的AJAX式请求,服务器端始终处于被动的应答状态,只有在客户端发出请求的情况下,服务器端才会返回响应。这种通信模式被称为客户端拉取(client pull)。在这种模式下,用户只能通过刷新页面或主动单击加载按钮来拉取新数据。 然而,在某些场景下,我们需要的通信模式是服务器端的主动推送(server push)。比如,一个聊天室有很多个用户,当某个用户发送消息后,服务器接收到这个请求,然后把消息推送给聊天室的所有用户。类似这种关注实时性的情况还有很多,比如社交网站在导航栏实时显示新提醒和私信的数量,用户的在线状态更新,股价行情监控、显示商品库存信息、多人游戏、文档协作等。 实现服务器端推送的一系列技术被合称为HTTP Server Push(HTTP服务器端推送),目前常用的推送技术如表2-14所示。  表2-14 常用推送技术 按照列出的顺序来说,这几种方式对实时通信的实现越来越完善。当然,每种技术都有各自的优缺点,在具体的选择上,要根据面向的用户群以及程序自身的特点来分析选择。这些技术我们会在本书第二部分的程序实例中逐一介绍。 轮询(polling)这类使用AJAX技术模拟服务器端推送的方法实现起来比较简单,但通常会造成服务器资源上的浪费,增加服务器的负担,而且会让用户的设备耗费更多的电量(频繁地发起异步请求)。SSE效率更高,在浏览器的兼容性方面,除了Windows IE/Edge,SSE基本上支持所有主流浏览器,但浏览器通常会限制标签页的连接数量。 > 附注 Server-Sent Event的最新标准可以在WHATWG([https://html.spec.whatwg.org/multipage/server-sent-events.html](https://html.spec.whatwg.org/multipage/server-sent-events.html) )查看,浏览器的支持情况可以在Can I use...([https://caniuse.com/#feat=eventsource](https://caniuse.com/#feat=eventsource) )查看。 除了这些推送技术,在HTML5的API中还包含了一个WebSocket协议,和HTTP不同,它是一种基于TCP协议的全双工通信协议(full-duplex communication protocol)。和前面介绍的服务器端推送技术相比,WebSocket实时性更强,而且可以实现双向通信(bidirectional communication)。另外,WebSocket的浏览器兼容性要强于SSE。 >附注 WebSocket协议在RFC 6455([https://tools.ietf.org/html/rfc6455](https://tools.ietf.org/html/rfc6455) )中定义,浏览器的支持情况可以在Can I use...([https://caniuse.com/#feat=websockets](https://caniuse.com/#feat=websockets) )查看。 如果你想进一步了解这几种推送技术的区别,StackOverflow的这篇答案[https://stackoverflow.com/a/12855533/5511849](https://stackoverflow.com/a/12855533/5511849) 对这几种推送技术进行了对比,并提供了直观的图示。 # 4、Web安全防范 无论是简单的博客,还是大型的社交网站,Web安全都应该放在首位。Web安全问题涉及广泛,我们在这里介绍其中常见的几种攻击(attack)和其他常见的漏洞(vulnerability)。 对于Web程序的安全问题,一个首要的原则是:永远不要相信你的用户。大部分Web安全问题都是因为没有对用户输入的内容进行“消毒”造成的。 ### 1.注入攻击 在OWASP(Open Web Application Security Project,开放式Web程序安全项目)发布的最危险的Web程序安全风险Top 10中,无论是最新的2017年的排名,2013年的排名还是最早的2010年,注入攻击(Injection)都位列第一。注入攻击包括系统命令(OS Command)注入、SQL(Structured Query Language,结构化查询语言)注入(SQL Injection)、NoSQL注入、ORM(Object Relational Mapper,对象关系映射)注入等。我们这里重点介绍的是SQL注入。 SQL是一种功能齐全的数据库语言,也是关系型数据库的通用操作语言。使用它可以对数据库中的数据进行修改、查询、删除等操作;ORM是用来操作数据库的工具,使用它可以在不手动编写SQL语句的情况下操作数据库。 > 附注 OWASP([https://www.owasp.org](https://www.owasp.org) )是一个开源的、非盈利的国际性安全组织。在OWASP网站的Top 10页面中的Translation Efforts标签([https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project) )下可以找到中文版本的Top 10报告。顺便说一句,我们在前面提及的开放重定向漏洞曾在2013 OWASP Top10中位列第10:Unvalidated Redirects and Forwards(未经验证的重定向或转发)。 ####(1)攻击原理 在编写SQL语句时,如果直接将用户传入的数据作为参数使用字符串拼接的方式插入到SQL查询中,那么攻击者可以通过注入其他语句来执行攻击操作,这些攻击操作包括可以通过SQL语句做的任何事:获取敏感数据、修改数据、删除数据库表…… ####(2)攻击示例 假设我们的程序是一个学生信息查询程序,其中的某个视图函数接收用户输入的密码,返回根据密码查询对应的数据。我们的数据库由一个db对象表示,SQL语句通过execute()方法执行: ``` @app.route('/students') def bobby_table(): password = request.args.get('password') cur = db.execute("SELECT * FROM students WHERE password='%s';" % password) results = cur.fetchall() return results ``` >注意 在实际应用中,敏感数据需要通过表单提交的POST请求接收,这里为了便于演示,我们通过查询参数接收。 我们通过查询字符串获取用户输入的查询参数,并且不经过任何处理就使用字符串格式化的方法拼接到SQL语句中。在这种情况下,如果攻击者输入的password参数值为“'or 1=1--”,即[http://example.com/students?password='or 1=1--](http://example.com/students?password='or%201=1--) ,那么最终视图函数中被执行的SQL语句将变为: ``` SELECT * FROM students WHERE password='' or 1=1 --;' ``` 这时会把students表中的所有记录全部查询并返回,也就意味着所有的记录都被攻击者窃取了。更可怕的是,如果攻击者将password参数的值设为“';drop table users;--”,那么查询语句就会变成: ``` SELECT * FROM students WHERE password=''; drop table students; --; ``` 执行这个语句会把students表中的所有记录全部删除掉。 在SQL中,“;”用来结束一行语句;“--”用来注释后面的语句,类似Python中的“#”。 #### (3)主要防范方法 1)使用ORM可以一定程度上避免SQL注入问题,我们将在第5章学习使用ORM。 2)验证输入类型。比如某个视图函数接收整型id来查询,那么就在URL规则中限制URL变量为整型。 3)参数化查询。在构造SQL语句时避免使用拼接字符串或字符串格式化(使用百分号或format()方法)的方式来构建SQL语句。而要使用各类接口库提供的参数化查询方法,以内置的sqlite3库为例: ``` db.execute('SELECT * FROM students WHERE password=?, password) ``` 4)转义特殊字符,比如引号、分号和横线等。使用参数化查询时,各种接口库会为我们做转义工作。 你可以访问OWASP的SQL注入页面([https://www.owasp.org/index.php/SQL_Injection](https://www.owasp.org/index.php/SQL_Injection) )了解详细的攻击原理介绍的防范措施。 ### 2.XSS攻击 XSS(Cross-Site Scripting,跨站脚本)攻击历史悠久,最远可以追溯到90年代,但至今仍然是危害范围非常广的攻击方式。在OWASP TOP 10中排名第7。 Cross-Site Scripting的缩写本应是CSS,但是为了避免和Cascading Style Sheets的缩写产生冲突,所以将Cross(即交叉)使用交叉形状的X表示。 #### (1)攻击原理 XSS是注入攻击的一种,攻击者通过将代码注入被攻击者的网站中,用户一旦访问网页便会执行被注入的恶意脚本。XSS攻击主要分为反射型XSS攻击(Reflected XSS Attack)和存储型XSS攻击(Stored XSS Attack)两类。 ####(2)攻击示例 反射型XSS又称为非持久型XSS(Non-Persistent XSS)。当某个站点存在XSS漏洞时,这种攻击会通过URL注入攻击脚本,只有当用户访问这个URL时才会执行攻击脚本。我们在本章前面介绍查询字符串和cookie时引入的示例就包含反射型XSS漏洞,如下所示: ``` @app.route('/hello') def hello(): name = request.args.get('name') response = '
Hello, %s!
' % name ``` 这个视图函数接收用户通过查询字符串传入的数据,未做任何处理就把它直接插入到返回的响应主体中,返回给客户端。如果某个用户输入了一段JavaScript代码作为查询参数name的值,如下所示: ``` http://example.com/hello?name= ``` 客户端接收的响应将变为下面的代码: ```
Hello, !
``` 当客户端接收到响应后,浏览器解析这行代码就会打开一个弹窗,如图2-15所示。  图2-15 被注入代码后的响应 你觉得一个小弹窗不会造成什么危害?那你就完全错了,能够执行alert()函数就意味着通过这种方式可以执行任意JavaScript代码。即攻击者通过JavaScript几乎能够做任何事情:窃取用户的cookie和其他敏感数据,重定向到钓鱼网站,发送其他请求,执行诸如转账、发布广告信息、在社交网站关注某个用户等。 即使不插入JavaScript代码,通过HTML和CSS(CSS注入)也可以影响页面正常的输出,篡改页面样式,插入图片等。 如果网站A存在XSS漏洞,攻击者将包含攻击代码的链接发送给网站A的用户Foo,当Foo访问这个链接就会执行攻击代码,从而受到攻击。 存储型XSS也被称为持久型XSS(persistent XSS),这种类型的XSS攻击更常见,危害也更大。它和反射型XSS类似,不过会把攻击代码储存到数据库中,任何用户访问包含攻击代码的页面都会被殃及。比如,某个网站通过表单接收用户的留言,如果服务器接收数据后未经处理就存储到数据库中,那么用户可以在留言中插入任意JavaScript代码。比如,攻击者在留言中加入一行重定向代码: ``` ``` 其他任意用户一旦访问留言板页面,就会执行其中的JavaScript脚本。那么其他用户一旦访问这个页面就会被重定向到攻击者写入的站点。 #### (3)主要防范措施 **a.HTML转义** 防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义,转义后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析。 这里的转义和Python中的概念相同,即消除代码执行时的歧义,也就是把变量标记的内容标记为文本,而不是HTML代码。具体来说,这会把变量中与HTML相关的符号转换为安全字符,以避免变量中包含影响页面输出的HTML标签或恶意的JavaScript代码。 比如,我们可以使用Jinja2提供的escape()函数对用户传入的数据进行转义: ``` from jinja2 import escape @app.route('/hello') def hello(): name = request.args.get('name') response = '
Hello, %s!
' % escape(name) ``` 在Jinja2中,HTML转义相关的功能通过Flask的依赖包MarkupSafe实现。 调用escape()并传入用户输入的数据,可以获得转义后的内容,前面的示例中,用户输入的JavaScript代码将被转义为: ``` <script>alert("Bingo!")</sript> ``` 转义后,文本中的特殊字符(比如“>”和“<”)都将被转义为HTML实体(character entitiy),这行文本最终在浏览器中会被显示为文本形式的,如图2-16所示。  图2-16 转义后的JavaScript代码输出 在Python中,如果你想在单引号标记的字符串中显示一个单引号,那么你需要在单引号前添加一个反斜线来转义它,也就是把它标记为普通文本,而不是作为特殊字符解释。在HTML中,也存在许多保留的特殊字符,比如大于小于号。如果你想以文本显示这些字符,也需要对其进行转义,即使用HTML字符实体表示这些字符。HTML实体就是一些用来表示保留符号的特殊文本,比如<;表示小于号,";表示双引号。 一般我们不会在视图函数中直接构造返回的HTML响应,而是会使用Jinja2来渲染包含变量的模板,这部分内容我们将在第3章学习。 **b.验证用户输入** XSS攻击可以在任何用户可定制内容的地方进行,例如图片引用、自定义链接。仅仅转义HTML中的特殊字符并不能完全规避XSS攻击,因为在某些HTML属性中,使用普通的字符也可以插入JavaScript代码。除了转义用户输入外,我们还需要对用户的输入数据进行类型验证。在所有接收用户输入的地方做好验证工作。在第4章学习表单时,我们会详细介绍表单数据的验证。 以某个程序的用户资料页面为例,我们来演示一下转义无法完全避免的XSS攻击。程序允许用户输入个人资料中的个人网站地址,通过下面的方式显示在资料页面中: ```
Website
``` 其中{{url}}部分表示会被替换为用户输入的url变量值。如果不对URL进行验证,那么用户就可以写入JavaScript代码,比如“javascript:alert('Bingo!');”。因为这个值并不包含会被转义的<和>。最终页面上的链接代码会变为: ```
Website
``` 当用户单击这个链接时,就会执行被注入的攻击代码。 另外,程序还允许用户自己设置头像图片的URL。这个图片通过下面的方式显示: ```
``` 类似的,{{url}}部分表示会被替换为用户输入的url变量值。如果不对输入的URL进行验证,那么用户可以将url设为“123"onerror="alert('Bingo!')”,最终的
标签就会变为: ```
``` 在这里因为src中传入了一个错误的URL,浏览器便会执行onerror属性中设置的JavaScript代码。 如果你想允许部分HTML标签,比如`
`和`
`,可以使用HTML过滤工具对用户输入的数据进行过滤,仅保留少量允许使用的HTML标签,同时还要注意过滤HTML标签的属性,我们会在本书的第二部分详细了解。 你可以访问OWASP的XSS页面([https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)) )了解详细的攻击原理介绍和防范措施。 ### 3.CSRF攻击 CSRF(Cross Site Request Forgery,跨站请求伪造)是一种近年来才逐渐被大众了解的网络攻击方式,又被称为One-Click Attack或Session Riding。在OWASP上一次(2013)的TOP 10 Web程序安全风险中,它位列第8。随着大部分程序的完善,各种框架都内置了对CSRF保护的支持,但目前仍有5%的程序受到威胁。 ### (1)攻击原理 CSRF攻击的大致方式如下:某用户登录了A网站,认证信息保存在cookie中。当用户访问攻击者创建的B网站时,攻击者通过在B网站发送一个伪造的请求提交到A网站服务器上,让A网站服务器误以为请求来自于自己的网站,于是执行相应的操作,该用户的信息便遭到了篡改。总结起来就是,攻击者利用用户在浏览器中保存的认证信息,向对应的站点发送伪造请求。在前面学习cookie时,我们介绍过用户认证通过保存在cookie中的数据实现。在发送请求时,只要浏览器中保存了对应的cookie,服务器端就会认为用户已经处于登录状态,而攻击者正是利用了这一机制。为了更便于理解,下面我们举一个实例。 ###(2)攻击示例 假设我们网站是一个社交网站(example.com),简称网站A;攻击者的网站可以是任意类型的网站,简称网站B。在我们的网站中,删除账户的操作通过GET请求执行,由使用下面的delete_account视图处理: ``` @app.route('/account/delete') def delete_account(): if not current_user.authenticated: abort(401) current_user.delete() return 'Deleted!' ``` 当用户登录后,只要访问[http://example.com/account/delete](http://example.com/account/delete) 就会删除账户。那么在攻击者的网站上,只需要创建一个显示图片的img标签,其中的src属性加入删除账户的URL: ```
``` 当用户访问B网站时,浏览器在解析网页时会自动向img标签的src属性中的地址发起请求。此时你在A网站的登录信息保存在cookie中,因此,仅仅是访问B网站的页面就会让你的账户被删除掉。 当然,现实中很少有网站会使用GET请求来执行包含数据更改的敏感操作,这里只是一个示例。现在,假设我们吸取了教训,改用POST请求提交删除账户的请求。尽管如此,攻击者只需要在B网站中内嵌一个隐藏表单,然后设置在页面加载后执行提交表单的JavaScript函数,攻击仍然会在用户访问B网站时发起。 虽然CSRF攻击看起来非常可怕,但我们仍然可以采取一些措施来进行防御。下面我们来介绍防范CSRF攻击的两种主要方式。 ### (3)主要防范措施 **a.正确使用HTTP方法** 防范CSRF的基础就是正确使用HTTP方法。在前面我们介绍过HTTP中的常用方法。在普通的Web程序中,一般只会使用到GET和POST方法。而且,目前在HTML中仅支持GET和POST方法(借助AJAX则可以使用其他方法)。在使用HTTP方法时,通常应该遵循下面的原则: * GET方法属于安全方法,不会改变资源状态,仅用于获取资源,因此又被称为幂等方法(idempotent method)。页面中所有可以通过链接发起的请求都属于GET请求。 * POST方法用于创建、修改和删除资源。在HTML中使用form标签创建表单并设置提交方法为POST,在提交时会创建POST请求。 在GET请求中,查询参数用来传入过滤返回的资源,但是在某些特殊情况下,也可以通过查询参数传递少量非敏感信息。 虽然在实际开发中,通过在“删除”按钮中加入链接来删除资源非常方便,但安全问题应该作为编写代码时的第一考量,应该将这些按钮内嵌在使用了POST方法的form元素中。正确使用HTTP方法后,攻击者就无法通过GET请求来修改用户的数据,下面我们会介绍如何保护GET之外的请求。 **b.CSRF令牌校验** 当处理非GET请求时,要想避免CSRF攻击,关键在于判断请求是否来自自己的网站。在前面我们曾经介绍过使用HTTP referer获取请求来源,理论上说,通过referer可以判断源站点从而避免CSRF攻击,但因为referer很容易被修改和伪造,所以不能作为主要的防御措施。 除了在表单中加入验证码外,一般的做法是通过在客户端页面中加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌(token)。 > 附注 在计算机语境中,令牌(token)指用于标记、验证和传递信息的字符,通常是通过一定算法生成的伪随机数,我们在本书后面会频繁接触到这个词。 在HTML中,POST方法的请求通过表单创建。我们把在服务器端创建的伪随机数(CSRF令牌)添加到表单中的隐藏字段里和session变量(即签名cookie)中,当用户提交表单时,这个令牌会和表单数据一起提交。在服务器端处理POST请求时,我们会对表单中的令牌值进行验证,如果表单中的令牌值和session中的令牌值相同,那么就说明请求发自自己的网站。因为CSRF令牌在用户向包含表单的页面发起GET请求时创建,并且在一定时间内过期,一般情况下攻击者无法获取到这个令牌值,所以我们可以有效地区分出请求的来源是否安全。 对于AJAX请求,我们可以在XMLHttpRequest请求首部添加一个自定义字段X-CSRFToken来保存CSRF令牌。 我们通常会使用扩展实现CSRF令牌的创建和验证工作,比如Flask-SeaSurf([https://github.com/maxcountryman/flask-seasurf](https://github.com/maxcountryman/flask-seasurf) )、Flask-WTF内置的CSRFProtect([https://github.com/lepture/flask-wtf](https://github.com/lepture/flask-wtf) )等,在后面我们会详细介绍具体的实践内容。 如果程序包含XSS漏洞,那么攻击者可以使用跨站脚本攻破可能使用的任何跨站请求伪造(CSRF)防御机制,比如使用JavaScript窃取cookie内容,进而获取CSRF令牌。 可以访问OWASP的CSRF页面([https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) )了解详细的攻击原理介绍的防范措施。 除了这几个攻击方式外,我们还有很多安全问题要注意。比如文件上传漏洞、敏感数据存储、用户认证(authentication)与权限管理等。这些内容我们将在后面的章节陆续介绍。 需要注意的是,虽然本书会介绍如何对常见的攻击和漏洞进行防御和避免,但仍然有许多其他的攻击和漏洞需要读者自己处理。另外,本书的示例程序(包括第一部分和第二部分)仅用于作为功能实现的示例,在安全方面并未按照实际运行的应用进行严格处理。比如,当单个用户出现频繁的登录失败时,应该采取添加验证码或暂时停止接收该用户的登录请求。请阅读OWASP或其他相关资料学习更多安全防御技巧。 你应该列出一个程序安全项目检查清单,可以参考OWASP Top 10或是CWE(Common Weakness Enumeration,一般弱点列举)提供的Top 25([https://cwe.mitre.org/top25/](https://cwe.mitre.org/top25/) )。确保你的程序所有的安全项目检查,也可以使用漏洞检查工具来,比如OWASP提供的WebScarab([https://github.com/OWASP/OWASP-WebScarab](https://github.com/OWASP/OWASP-WebScarab) )。