Django之安全编码规范
作为优秀的WEB
开发人员,每位开发者都应该重视代码的安全性。这也是开发中很大的难点,令人兴奋的是Django
框架尝试为我们减轻这个难点,它的设计就是为我们防护WEB
开发中易犯的安全问题。
SQL
注入
所谓SQL
注入,就是通过把SQL
命令插入到WEB表单或页面请求的查询字符串中,最终欺骗数据库服务器执行恶意的SQL
命令。
攻击原理
这个问题最容易出现在用户手动输入参数,构造SQL
查询语句,而站点不对数据做任何 处理的情况。例如,设想写一个方法来从搜索页面获取某用户的联系列表,要求用户输入他的用户名:
1 | def user_contacts(request): |
如果用户输入
' or 'a'='a
到查询框里,sql
查询语句将变为:1
'select id,title from app_category where title = '' or 'a'='a'
结果:返回所有用户联系的数据。
如果用户输入
';DELETE FROM user_contacts WHERE 'a' = 'a
到查询框里,sql
查询语句将变为:1
SELECT * FROM user_contacts WHERE username = ''; DELETE FROM user_contacts WHERE 'a' = 'a';
结果:清空
user_contacts
表中所有的数据。
不对用户提交的数据做任何处理就直接构造SQL
语句,这样会允许用户构造出畸形的查询语句,让数据库执行非授权的命令,甚至获得数据库所在服务器的访问权限。后果相当严重。
解决方案
很简单,绝不信任用户提交的数据,并且总是转义传递过来的SQL
语句。使用Django
自带的数据库API
,它会根据所使用的数据库服务器(例如PostgreSQL
或者MySQL
)的转换规则,自动转义特殊的SQL
参数。
使用Django
的ORM
进行字段查询
使用Django
底层数据库API
的查询
1 | from django.db import connection |
使用Django
的ORM
执行自定义sql
语句
1 | user = request.GET['username'] |
在 cursor.execute()
, ModelInstance.objects.raw()
中使用第二个参数列表,Django
会 自动转义传递过来的数据,防止SQL
注入攻击。
如果只是简单的查询数据,建议使用第一种方式。如果要构造复杂的查询语句,建议使用第三种方式。
此外,使用
extra()
方法是要注意: 当extra()
方法使用params
参数,Django
会自动转义数据。
1 | data = "apple" |
除了使用 Django
自带的 API
来执行SQL
语句,公司很多的项目会使用 CPgSqlParam
来构建 SQL
语句:
1 | sql = "select id, name from %s" % (TBL_PLAYBOOK) |
注意:除非是常量,否则请无论如何都不要像上述代码一样直接拼接,代码审计的过程中发现过无数的自认安全拼接但实际上可以
SQL
注入的问题,一定要采用参数化的执行:
1 | get_sql = '''select id from internal_app_soarmgr.playbook where name=%s ''' |
重点检查:如果使用表单(或url
方式)传递过来的数据动态构建SQL
语句,请使用Modellnstance.objects.raw(sql, [ ])
、connection.cursor().execute(sql ,[])
这几种方式。
XSS
攻击(跨站脚本攻击)
XSS
漏洞的成因其实就是HTML
代码注入,用户输入的数据没有经过严格处理就存储 到了数据库中,又没有经过过滤处理,直接就读取出来显示到网页中,导致访问用户的浏览 器执行了这段HTML
代码。
攻击原理
跨站脚本攻击常发生在以下几种场合:
在
HTML
标签中输出变量如:1
<div>hello, {{var}}</div>
在
HTML
属性中输出变量如:1
<img src="{{var}} " alt=''/>
在
script
标签中输出变量如:``1
<script type='text/javascript'>{{var}}</script>
在事件中输出变量如:
1
<img src="#" onerror={{var}}>
在
CSS
中输出变量如:1
2<img src="#" style="{{var}}">
var = "Xss:expression(alert('see you'));"在
url
中输出变量如:1
http://www.iseeyou.com?{{var}}
解决方案(Django
)
Django
默认自动转义
如果我们对这些插入到HTML
中的变量进行严格的过滤,恶意代码就不会被执行。令人 兴奋的是Django
默认情况下,已经为我们自动转义了每一个变量标签的输出。
尤其是下面的5个字符:
1 | <被转义成< |
例如:This will be escaped: {{data}}
如果 data
为<strong>xss</strong>
,在浏览器中显示:This will be escaped: <strong>xss</strong>
注意:强烈建议禁止关闭
Django
自动转义功能,除非明确传入的数据是安全的,并务必在 代码中添加详细的说明。
关闭自动转义
有些时候我们需要模板输出HTML代码,而非转义后的代码。我们可以使用以下两种方 式关闭Django
的自动转义;
使用safe过滤器
1
This will not be escaped: {{data|safe}}
使用
autoescape
标签1
2
3{% autoescape off %}
This will not be escaped: {{data}}
{% endautoescape %}如果
data
为<strong>xss</strong>
,在浏览器中显示:加粗后的xss autoescape
标签有两个参数:off
和on
,autoescape
标签可以嵌套使用。有时我们需要这样:1
2
3
4
5
6
7{% autoescape off %} <!--关闭自动转义-->
This will not be auto-escaped: {{ data }}.
Nor this: {{ other_data }}
{% autoescape on %} <!--开启自动转义-->
Auto-escaping applies again: {{name}}
{% endautoescape %}
{% endautoescape %}1
重点检查:`Django` 视图模板中是否使用了`{{data|safe}}`、`{% autoescape off %}...{% endautoescape %}`标签
解决方案(jinja2
)
模板变量转义
与Django
类似,Jinja2
提供模板变量转义功能,并支持手动转义与自动转义两种方式
自动转义
通过Environment
或者Template
的构建器传递autoescape
参数,可以设置是否自动转义,如下所示:
1 | jj = Environment(extensions=global_exts, loader=ChoiceLoader(loader_array), undefined = undefined, autoescape = True) |
autoescape
设置为True
即可实现全局自动转义,其中,autoescape
默认为False。
手动转义
Jinja2
可以通过以下方式实现手动转义:{{ user.username | e }}
其中,’ e’就是转义过滤器
注意:强烈建议开启
jinja2
转义功能,除非明确传入的数据是安全的,并务必在代码中添 加详细的说明。
jinja2
取消自动转义(前提是已开启全局自动转义功能)
可以通过autoescape
设置取消自动转义
如下所示:
1 | jj = Environment(extensions=global_exts, loader=ChoiceLoader(loader_array), undefined = undefined, autoescape = False) |
这种方式实现全局取消自动转义。
在打印变量时,使用内置过滤器’safe’实现取消转义
如下所示:
1 | {{ user.username | safe }} |
在模板中使用标签取消转义
1 | {% raw %}{% endraw %} |
如下所示:
1 | {% raw %}{{data}}{% endraw %} |
1 重点检查:jinja2视图模板中是否使用了{{data|safe}}、{% raw %}{% endraw %}标签,以 及Environment()函数中的参数autoescape值是否为False。
CSRF
(跨站请求伪造)
CSRF
是一种依赖web浏览器的、被混淆过的代理人攻击,通过在授权用户访问的页面 中包含链接或者脚本的方式工作。
攻击原理
例如:一个网站用户A正在浏览BBS
论坛,而同时另一个用户B在此论坛中发表了一 个帖子,这个帖子中具有用户A银行链接的图片消息<img src=url />
,这个url
是用户A的银行站点上进行取款的form
链接。如果用户A的浏览器保存着银行站点的cookie
,该cookie
保存了用户A的授权信息,并且没有过期。当用户A的浏览器尝试装载该图片时,将会提交 这个取款form
和cookie
,在没有经过用户A同意的情况下便授权了这次事务。
解决方案
使用Django
的CSRF
中间件,开启Django
的CSRF
保护
第一步:在Django
项目的settings.py
文件中添加中间件’CsrfViewMiddleware
‘。
1 | MIDDLEWARE_CLASSES =( |
第二步:在form
表单中添加
1 | {% csrf_token %} |
1 | <form method="post" action=""> |
第三步:在views.py
中,生成csrf_token
,并将其添加到模板中。
第一种方式:
1 | from django. core. context_processors import csrf |
第二种方式:
1 | from django.shortcuts import Requestcontext |
‘CsrfViewMiddleware
‘中间件主要完成了两项任务:修改当前处理的请求,向所有的POST表单增添一个隐藏的表单字段<input type="hidden" name="csrfmiddlewaretoken" value="散列值" >
,使用名称是 csrfmiddlewaretoken
,值为当前会话ID加上一个密钥的散列值。如果未设置会话 ID,该中间件将不会修改响应结果,因此对于未使用会话的请求来说性能损失是可以忽略的。1、对于所有含会话cookie集合的POST请求,它将检查是否存csrfmiddlewaretoken
及其是否正确。如果不存在或不正确,用户将会收到一个403 HTTP错误而终止请求。
重点检查:
Django
项目的settings.py
中是否添加了中间件’django.middleware.csrf.CsrfViewMiddleware
‘;- 视图模板中的表单涉及到
POST
请求的(包括Ajax
的post
请求)是否添加了csrf_token
; - 如果传递的数据比较重要,请使用
POST
请求方式,而非GET。
Clickjacking
(点击劫持)
攻击原理
点击劫持是一种视觉上的欺骗手段。例如攻击者使用一个透明的、不可见的iframe
,覆盖在一个网页上,然后诱使用户在该网页上进行操作,此时用户将在不知情的情况下点击透 明的iframe
页面,执行一些操作。还有Adobe
的点击劫持案例,攻击者利用有漏洞版本的 Flash
播放器软件,通过一个游戏界面的操作,打开了接在用户电脑上的摄像头和麦克风,进行窥探用户。
解决方案
使用HTTP
头X-Frame-Options
,防止我们的站点被其它站点以iframe
的形式加载,目 前有以下浏览器支持X-Frame-Options
:
IE 8+
Opera 10.50+
Safari 4+
Chrome 4.1.249.1042+
Firefox 3.6.9+ (or earlier with NoScript)
X-Frame-Options
有三个可选的值:DENY
、SAMEORIGIN
、ALLOW-FROM
若值为DENY
,浏览器会拒绝任何iframe
加载src
指定的页面;
若值为SAMEORIGIN
,浏览器只允许iframe
加载src
为同源域名下的页面;
若值为ALLOW-FROM
,浏览器允许iframe
加载src
指定的页面。
在Django
应用程序中使用X-Frame-Options
。
注:django 1.6
版本中默认开启了点击劫持中间件
第一步: 在 Django
项目的 settings.py
文件中添加中间件’XFrameOptionsMiddleware
‘
1 | MIDDLEWARE_CLASSES = ( |
第二步:设置X_FRAME_IPTIONS
的值
1 | X_FRAME_OPTIONS = ' SAMEORIGIN ' # 建议为SAMEORIGIN |
注意:对于不支持X-Frame-Options
的浏览器,我们需要采取其它措施来阻止Clickjacking
。
推荐一种方式:
在HTML的head层最底部添加代码:
1 | <style id="antiClickjack"> |
实现原理:防止当前页面被嵌套在iframe
中。如果被嵌套在iframe
中,将不显示该页面。 结合这两种方案,可以有效的防止我们的站点被其它站点以iframe
的形式加载。
重点检查:
- 检查
Django
项目的settings.py
中是否添加了中间件’django.middleware.clickjacking.XFrameOptionsMiddleware
‘; - 检查
X_FRAME_OPTIONS
的值是否为”DENY
“或 “SAMEORIGIN
“; - 检查是否对低版本浏览器做了
Clickjacking
防护。
Session
伪造/截取
攻击原理
这是一个特殊的攻击,而不是对用户session
数据的一般类型的攻击,它有多种形式:
中间人攻击,其中攻击者在有线(或者无线)网络上窃听
session
数据。session
伪造,其中攻击者使用伪造的session ID
(可能通过中间人攻击获得)来假装为 另外一个用户。cookie
伪造攻击,攻击者覆盖存储在cookie
中的数据
解决方案
Django
提供的session
框架,很好的避免了 cookie
和持续性会话引发的诸多WEB
安全 问题。我们可以用session
框架来存取每个访问者的任意数据,这些数据存储在服务器端, 并对cookie
的收发进行了抽象。cookie
只存储数据的哈希会话ID
,而不是数据本身,从而 避免了大部分的常见cookie
安全问题。
使用Django
的SessionMiddleware
中间件,开启Django
的会话保护。
第一步:在Django
项目的settings.py
文件中添加中间件SessionMiddleware
。
1 | MIDDLEWARE_CLASSES =( |
第二步:确认 INSTALLED_APPS
中有’django.contrib.sessions
‘,如果刚开启该应用, 需要运行 manage.py syncdb
重点检查:
Django
项目的settings.py
中是否添加了中间件django.contrib.sessions.middleware.SessionMiddleware
;
E-mail
头部注入
攻击原理
任何从web表单数据构建email头部的形式都是这种类型的攻击。如果当构建email信息时头部没有进行过滤,攻击者可以使用类似于”hello\ncc:spamvictim@example.com
“(这里\n
是换行字符),这将使得构建的email头部变成:
1 | To: hardcoded@example.com |
和SQL
注入一样,如果我们信任用户给定的数据,我们将允许用户构建一些恶意的头部。 他们就可以使用我们的联系表单来发送垃圾邮件。
解决方案
可以用我们预防SQL
注入同样的方式来防止这种攻击:验证并过滤用户提交的内容。Django
内建的mail
方法(位于django.core.mail
)不允许用户构建头部(发送和接收地址以 及主题)的任何域中有换行。如果尝试使用django.core.mail.send_mail
和一个包含换行的数 据,Django
将触发 BadHeaderError
异常。
重点检查:是否对邮件头部,主题数据过滤了换行符和其它非法字符;发送邮件 时,建议使用
django.core.mail.send_mail
函数。
使用 HTTPS
HTTPS
协议是由SSL+HTTP
协议构建的可进行加密传输、身份认证的网络协议,而HTTP
协议,信息都是以明文传输的。
可以通过配置
Apache
服务器,来决定站点的某些路径下的访问(如:/accounts/
), 或者具体的某个链接(如:/accounts/login
)需要以HTTPS
来访问。或者整个站点 都以HTTPS
的形式访问。设置重定向,避免页面错误
在Apache配置文件中配置:
1
2
3
4<Location /admin>
RewriteRule (.*) https://example.com/$1 [L,R=301]
...
</Location>这样确保当用户访问
http://example.com/admin
时,重定向回https://example.com/admin
或者在
Django
中编写中间件,判断当前的请求是否为https
,如果不是,自动重 定向到https
。或者在
views
中使用request.is_secure()
来判断请求是否为https
,如果不是,自动重定向到https
。
安全的传递
cookie
设置SESSION_COOKIE_SECURE
和CSRF_COOKIE_SECURE
为True
。确保浏 览器仅通过https
发送cookie
,而且CSRF
保护组件会阻止http
下以post
方式发送 数据。
重点检查:
Django
项目的settings.py
文件中的SESSION_COOKIE_SECURE
和CSRF_COOKIE_SECURE
是否设为了True
。
文件上传
检查文件的类型,设置文件类型白名单。
检查上传文件的大小,防止过大的文件上传,引发DOS(拒绝服务式)攻击,
我们可以设置
Apache
的LimitRequestBody
LimitRequestBody 0
可以限制文件大小的范围是:0 (无限制) to 2147483647 (
2GB
)Django
中静态目录的设置一般产品发布在Apache的环境下,
Django
静态目录的设置也在Apache中配置(这时 候Django
的settings.py
中设置的静态目录配置将不起作用)。建议配置Apache
时, 如果产品中没有执行使用类似php
,asp
脚本文件,Apache
不要配置php
,asp等的解 析模块,以防恶意用户利用公共目录执行非法的脚本文件。
重点检查:
- 上传文件的名称进行重命名;
- 限制上传文件的类型;
- 限制上传文件的大小;
- 建议在
Apache
配置文件中设置LimitRequestBody
的值,严格限制上传文件的大小- 如果产品中没有执行使用
php
,asp
等脚本文件,建议Apache
不要配置php
,asp
等的解析模块;
错误信息
产品发布后,要避免暴漏给用户任何错误信息,Django
有简单的标记来控制这些错误的 显示。
在Django
项目的settings.py
文件中:
关闭
Debug
模式:DEBUG = False
如果为
True
,所有的数据库查询将被保存在内存中;任何404错误都将呈现django
的特殊的404页面;程序中任何未捕获的异常,从基本的python
语法错误到 数据库错误以及模板语法错误,都将返回详细的错误信息。关闭模板
Debug
模式:TEMPLATE_DEBUG = False
如果为
True
,为了在错误页面上显示足够的东西,Django
的模版系统会为每一 个模版保存一些额外的信息。关闭
Apache
配置文件中的python
调试选项确保任何在
Django
有机会载入之前发生的错误都将不会显示给公众。
重点检查:
Django
项目的settings.py
中将DEBUG
和TEMPLATE_DEBUG
的值设为False
;- 关闭
Apache
配置文件中的python
调试选项;- 总之,关闭其它所有
Debug
模式。
目录穿越
目录穿越是另一种注入风格的攻击,恶意用户通过构建代码,欺骗文件系统来读或写WEB 服务器不允许访问的文件。
1 | def dump_file(request): |
尽管它看起来限制了文件访问BASE_PATH
(通过使用os.path.join
)下面的文件,如果攻击者传递一个包含”..”(这两个句点是UNIX
对”父目录”的捷径)的filename
,他就可以访问 BASE_PATH
之上的文件,比如../../../../../etc/passwd
如果程序是基于用户的输入来读写文件,要对用户的请求路径进行严格验证和过滤,来 确保攻击者不能从限制访问的基本目录逃离。
对于目录穿越的防御,过滤通常采用以下2种过滤方式:
使用”
basename
“函数过滤:1
2
3
4def dump_file(request):
filename = basename(request.GET["filename"])
filename = os.path.join(BASE_PATH, filename)
content = open(filename).read()过滤文件名中所有的”
..
“:1
2
3
4
5def dump_file(request):
filename = request.GET["filename"]
filename = filename.replace("..","")
filename = os.path.join(BASE_PATH, filename)
content = open(filename).read()
设置权限
- 文件权限
- 去掉WEB用户对常用、重要系统命令的读写执行权限;
- 去掉其它用户对apache日志的读权限;
- 去掉WEB上传目录其它脚本语言的解析权限;
- 数据库权限
- 给用户授予其所需要的最小权限;
- 取消默认账户不需要的权限;
Sameorigin
(同源)
简单的讲,同源就是要求域名,协议,端口三者都一致。
先了解一下域名,比如 https://www.diandian100.cn:
diandian100.cn 主域名
www.diandian100.cn二级域名
img.diandian100.cn 二级域名
(www.diandian100.cn、img.diandian100.cn)虽然属于同一主域,但是子域不同,所以它们非同源。