安全注意事项¶
Web 应用通常会面临各种安全问题,很难把他们全都处理好。Flask 试图为你解决其中的一些问题,但还有很多需要你自己处理。
跨站脚本攻击(XSS)¶
跨站脚本攻击是指将恶意的 HTML(以及附带的 JavaScript)代码注入到网站的环境中。为了解决这个问题,开发人员必须正确地转义文本,使其无法包含恶意的 HTML 标签。更多信息请看维基百科上的文章 Cross-Site Scripting。
Flask 配置了 Jinja2 自动转义所有值,除非显式指明不转义。这应该可以排除所有由模板引起的 XSS 问题,但仍有其他地方需要注意:
不使用 Jinja2 生成 HTML
在用户提交的数据上调用
Markup
发送上传的 HTML 文件,千万不要这样做,应该使用
Content-Disposition: attachment
头部来避免这个问题。发送上传的文本文件。一些浏览器会基于文件开头的几个字节推测文件的内容类型,因此用户可以欺骗浏览器执行 HTML。
另外值得注意的是未经引号包裹的属性。虽然 Jinja2 可以通过转义 HTML 避免 XSS 问题,但无法避免通过属性注入的 XSS。为了对付这种可能存在的攻击手段,在属性中使用 Jinja 表达式时,请保证属性值始终被包括在双引号或单引号内:
<input value="{{ value }}">
为什么必须这样做?因为如果不这样做,攻击者可以轻易注入定制的 JavaScript 事件处理器。例如,攻击者可以注入以下 HTML + JavaScript 代码:
onmouseover=alert(document.cookie)
当用户将鼠标移到输入框时,cookie 信息就会显示在弹出的警告窗口中。 一个聪明的攻击者可能不会直接向用户显示 cookie 信息,而是执行其他的 JavaScript 代码。 结合 CSS 注入,攻击者甚至可能使该元素填满整个页面,这样用户将鼠标放在页面的任何位置都会触发攻击。
有一类 XSS 问题,Jinja 的转义并不能防止。a
标签的 href
属性可以包含一个 javascript: URI,如果没有采取合理的安全措施,在点击时浏览器就会执行。
<a href="{{ value }}">click here</a>
<a href="javascript:alert('unsafe');">click here</a>
为了防止这种问题发生,你需要设置 内容安全策略(CSP) 响应头部。
跨站请求伪造(CSRF)¶
另一个大问题是跨站请求伪造。这是一个很复杂的话题,细节不在这里赘述,只谈谈它是什么以及如何在理论上避免这个问题。
如果你的认证信息存储在 cookie 中,你就有了隐式状态管理。“登录”的状态是由一个 cookie 控制的,对每个页面的请求都会带上这个 cookie。不幸的是,由第三方网站发起的请求也遵循同样的机制。如果你不牢记这一点,有人可能会用社交工程欺骗应用程序的用户,让他们在不知情的情况下做一些愚蠢的事情。
假设你有一个特定的 URL,当你发送 POST
请求时会删除一个用户的资料(例如 http://example.com/user/delete
)。 如果攻击者现在创建了一个页面可以通过一些 JavaScript 代码向你的特定 URL 发送 post 请求,只要欺骗用户来加载这个页面,用户资料就会被删除了。
想象一下有数百万并发用户的 Facebook,有人发出小猫咪图片的链接。 当用户访问该页面看着毛茸茸的猫片时,他们的资料就被删除了。
如何防止这种情况呢?基本思路是:每一个修改服务器内容的请求,必须使用一个一次性令牌,并将其存储在 cookie 中,传输表单数据时带上它。服务器再次接收到数据后,必须比较两个令牌,确保它们是相等的。
为什么 Flask 不替你完成这个呢?因为这是表单验证框架要做的事,而 Flask 自身不包含表单验证。
JSON 安全¶
在 Flask 0.10 以及更低版本中,因为 ECMAScript 4 中的安全漏洞,jsonify()
没有将顶层数组序列化为 JSON。
ECMAScript 5 关闭了这个漏洞,所以只有非常老的浏览器仍然容易受到攻击,这些浏览器还存在 其他更严重的漏洞,因而这个行为被改变了,jsonify()
现在支持序列化数组。
安全头部¶
为了控制安全性,浏览器识别各种响应头部。我们建议你在应用程序中检查以下每种头部,Flask-Talisman 扩展可用于管理 HTTPS 和安全头部。
HTTP 严格传输安全(HSTS)¶
指示浏览器把所有 HTTP 请求转为 HTTPS,防止中间人攻击(MITM)。
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
内容安全策略(CSP)¶
告诉浏览器可以从哪里加载各种类型的资源。应当尽可能使用这个头部,但需要为网站定义正确的策略。一个非常严格的策略是:
response.headers['Content-Security-Policy'] = "default-src 'self'"
X-Content-Type-Options¶
强制浏览器接受响应的内容类型,而不是尝试检测它,防止被滥用而产生跨站脚本攻击(XSS)。
response.headers['X-Content-Type-Options'] = 'nosniff'
X-Frame-Options¶
防止外部网站将你的网站嵌入 iframe
中,可以避免一类攻击:对外框的点击无形地转化为对你的页面元素的点击。这也被称为“点击劫持”。
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
Set-Cookie 选项¶
下面这些选项可以添加到 Set-Cookie
头部中以提高安全性。Flask 有配置选项可以将这些设置在会话 cookie 或者其他 cookie 上。
Secure
限制 cookies 只用于 HTTPS 流量。HttpOnly
保护 cookies 内容不被 JavaScript 读取。SameSite
限制 cookies 如何与外部网站的请求一起发送。可设置为’Lax’``(推荐)或 ``’Strict’
。Lax
防止外部网站有 CSRF 请求倾向时(例如提交表单)发送 cookies。Strict
禁止所有外部请求发送 cookie,包括常规链接。
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
)
response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax')
指定 Expires
或 Max-Age
选项时,cookie 会在超过了给定的时间或当前时间加上的期限之后被删除。如果两个选项都没有设置,cookie 将在浏览器关闭时被删除。
# cookie expires after 10 minutes
response.set_cookie('snakes', '3', max_age=600)
对于会话 cookie,如果设置了 session.permanent
,那么就用 PERMANENT_SESSION_LIFETIME
设置到期时间,Flask 的默认 cookie 实现会验证加密签名不超过这个值,降低此值有助于减少重复攻击,防止 cookie 被拦截后还可以在一段时间内有效发送的漏洞。
app.config.update(
PERMANENT_SESSION_LIFETIME=600
)
@app.route('/login', methods=['POST'])
def login():
...
session.clear()
session['user_id'] = user.id
session.permanent = True
...
使用 itsdangerous.TimedSerializer
来签名和验证其他 cookie 值(或任何需要安全签名的值)。
复制/粘贴到终端¶
隐藏的字符如退格符(\b
, ^H
)会导致文本在 HTML 中的显示方式与 粘贴到终端 的呈现方式不同。
例如,import y\bose\bm\bi\bt\be\b
在 HTML 中显示为 import yosemite
,但在粘贴到终端时退格符生效,变成 import os
.
如果用户有可能从你的网站上复制和粘贴不受信任的代码,比如在技术博客上发布的评论,要考虑使用更多的过滤条件,例如替换所有 \b
字符。
body = body.replace("\b", "")
大多数现代终端在粘贴时都会警告并删除隐藏的字符,所以严格来说这并不是必须的。还有以其他方式生成危险命令的可能,这些都是不可能完全过滤的。根据你的网站的使用情况来做决定,复制代码时显示一个一般警告也是可以的。