Django之安全编码规范

作为优秀的WEB开发人员,每位开发者都应该重视代码的安全性。这也是开发中很大的难点,令人兴奋的是Django框架尝试为我们减轻这个难点,它的设计就是为我们防护WEB 开发中易犯的安全问题。

SQL注入

所谓SQL注入,就是通过把SQL命令插入到WEB表单或页面请求的查询字符串中,最终欺骗数据库服务器执行恶意的SQL命令。

攻击原理

这个问题最容易出现在用户手动输入参数,构造SQL查询语句,而站点不对数据做任何 处理的情况。例如,设想写一个方法来从搜索页面获取某用户的联系列表,要求用户输入他的用户名:

1
2
3
def user_contacts(request):
username = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = '%s';" % username
  • 如果用户输入' 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参数。

使用DjangoORM进行字段查询

使用Django底层数据库API的查询

1
2
3
4
5
6
7
8
9
from django.db import connection
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s"
cursor = connection.cursor()
cursor.execute(sql, [user])
# do something with the results
results = cursor.fetchone() #or results = cursor.fetchall()
cursor.close()

使用DjangoORM执行自定义sql语句

1
2
3
4
5
6
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s"
result = ModelInstance.objects.raw(sql, [user]) #ModelInstance 为模型实例
for person in result:
print(person.age)
#do something with the results

cursor.execute() , ModelInstance.objects.raw()中使用第二个参数列表,Django会 自动转义传递过来的数据,防止SQL注入攻击。

如果只是简单的查询数据,建议使用第一种方式。如果要构造复杂的查询语句,建议使用第三种方式。

此外,使用 extra()方法是要注意: 当 extra()方法使用 params 参数,Django 会自动转义数据。

1
2
3
4
5
6
7
8
data = "apple"
ModelInstance.objects.extra(where=['headline=%s'], params=[data]) # 正确

ModelInstance.objects.extra(where=["headline='apple'"]) # 不建议使用

user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s" % user
ModelInstance.objects.raw(sql) # 不建议使用

除了使用 Django 自带的 API 来执行SQL语句,公司很多的项目会使用 CPgSqlParam来构建 SQL 语句:

1
2
sql = "select id, name from %s" % (TBL_PLAYBOOK)
rets = json.loads(CFunction.execute(CPgSqlParam(sql)))

注意:除非是常量,否则请无论如何都不要像上述代码一样直接拼接,代码审计的过程中发现过无数的自认安全拼接但实际上可以 SQL 注入的问题,一定要采用参数化的执行:

1
2
get_sql = '''select id from internal_app_soarmgr.playbook where name=%s '''
CFunction.execute(CPgSqlParam(get_sql, params=[pb_name]))

重点检查:如果使用表单(或url方式)传递过来的数据动态构建SQL语句,请使用Modellnstance.objects.raw(sql, [ ])connection.cursor().execute(sql ,[])这几种方式。

XSS攻击(跨站脚本攻击)

XSS漏洞的成因其实就是HTML代码注入,用户输入的数据没有经过严格处理就存储 到了数据库中,又没有经过过滤处理,直接就读取出来显示到网页中,导致访问用户的浏览 器执行了这段HTML代码。

攻击原理

跨站脚本攻击常发生在以下几种场合:

  1. HTML标签中输出变量如:

    1
    <div>hello, {{var}}</div>
  2. HTML属性中输出变量如:

    1
    <img src="{{var}} " alt=''/>
  3. script标签中输出变量如:``

    1
    <script type='text/javascript'>{{var}}</script>
  4. 在事件中输出变量如:

    1
    <img src="#" onerror={{var}}>
  5. CSS中输出变量如:

    1
    2
    <img src="#" style="{{var}}">
    var = "Xss:expression(alert('see you'));"
  6. url中输出变量如:

    1
    http://www.iseeyou.com?{{var}}

解决方案(Django)

Django默认自动转义

如果我们对这些插入到HTML中的变量进行严格的过滤,恶意代码就不会被执行。令人 兴奋的是Django默认情况下,已经为我们自动转义了每一个变量标签的输出。

尤其是下面的5个字符:

1
2
3
4
5
<被转义成&lt;
>被转义成&gt;
'被转义成&#39;
"被转义成&quot;
&被转义成&amp;

例如: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标签有两个参数:offon, 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的浏览器尝试装载该图片时,将会提交 这个取款formcookie,在没有经过用户A同意的情况下便授权了这次事务。

解决方案

使用DjangoCSRF中间件,开启DjangoCSRF保护

第一步:在Django项目的settings.py文件中添加中间件’CsrfViewMiddleware‘。

1
2
3
MIDDLEWARE_CLASSES =(
'django. middleware.csrf. CsrfViewMiddleware'
)

第二步:在form表单中添加

1
{% csrf_token %}
1
2
3
<form method="post" action="">
{% csrf_token %}
</form>

第三步:在views.py中,生成csrf_token,并将其添加到模板中。

第一种方式:

1
2
3
4
5
6
7
from django. core. context_processors import csrf
from django.shortcuts import render_to_response
def my_view(request):
c = 0
c.update(csrf(request))
#... view code here
return render_to_response("a_template.html" c)

第二种方式:

1
2
3
4
5
from django.shortcuts import Requestcontext
from django.shortcuts import render_to_response
def my_view(request):
# ... view code here
return render_to_response("a_template.html", context_instance=RequestContext(request))

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请求的(包括Ajaxpost请求)是否添加了csrf_token
  • 如果传递的数据比较重要,请使用POST请求方式,而非GET。

Clickjacking(点击劫持)

攻击原理

点击劫持是一种视觉上的欺骗手段。例如攻击者使用一个透明的、不可见的iframe,覆盖在一个网页上,然后诱使用户在该网页上进行操作,此时用户将在不知情的情况下点击透 明的iframe页面,执行一些操作。还有Adobe的点击劫持案例,攻击者利用有漏洞版本的 Flash播放器软件,通过一个游戏界面的操作,打开了接在用户电脑上的摄像头和麦克风,进行窥探用户。

解决方案

使用HTTPX-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有三个可选的值:DENYSAMEORIGINALLOW-FROM

若值为DENY,浏览器会拒绝任何iframe加载src指定的页面;

若值为SAMEORIGIN,浏览器只允许iframe加载src为同源域名下的页面;

若值为ALLOW-FROM,浏览器允许iframe加载src指定的页面。

Django应用程序中使用X-Frame-Options

注:django 1.6版本中默认开启了点击劫持中间件

第一步: 在 Django 项目的 settings.py 文件中添加中间件’XFrameOptionsMiddleware

1
2
3
4
MIDDLEWARE_CLASSES = (
...
'django.middleware.clickjacking.XFrameOptionsMiddleware'
)

第二步:设置X_FRAME_IPTIONS的值

1
X_FRAME_OPTIONS = ' SAMEORIGIN '	# 建议为SAMEORIGIN

注意:对于不支持X-Frame-Options的浏览器,我们需要采取其它措施来阻止Clickjacking

推荐一种方式:

在HTML的head层最底部添加代码:

1
2
3
4
5
6
7
8
9
10
11
<style id="antiClickjack">
body{display:none important;}
</style>
<script type="text/javascript">
if (self === top) (
var antiClickjack = document.getElementById("antiClickjack");
antiClickjack.parentNode.removeChild(antiClickjack);
} else (
top.location = self.location;
}
</script>

实现原理:防止当前页面被嵌套在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安全问题。

使用DjangoSessionMiddleware中间件,开启Django的会话保护。

第一步:在Django项目的settings.py文件中添加中间件SessionMiddleware

1
2
3
MIDDLEWARE_CLASSES =(
django.contrib.sessions.middleware.SessionMiddleware
)

第二步:确认 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
2
3
To: hardcoded@example.com
Subject: hello
cc: spamvictim@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 协议,信息都是以明文传输的。

  1. 可以通过配置Apache服务器,来决定站点的某些路径下的访问(如:/accounts/), 或者具体的某个链接(如:/accounts/login)需要以HTTPS来访问。或者整个站点 都以HTTPS的形式访问。

  2. 设置重定向,避免页面错误

    • 在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

  3. 安全的传递

    cookie设置 SESSION_COOKIE_SECURECSRF_COOKIE_SECURETrue。确保浏 览器仅通过https发送cookie,而且CSRF保护组件会阻止http下以post方式发送 数据。

重点检查:Django 项目的 settings.py 文件中的 SESSION_COOKIE_SECURECSRF_COOKIE_SECURE 是否设为了 True

文件上传

  1. 检查文件的类型,设置文件类型白名单。

  2. 检查上传文件的大小,防止过大的文件上传,引发DOS(拒绝服务式)攻击,

    我们可以设置 ApacheLimitRequestBody

    LimitRequestBody 0

    可以限制文件大小的范围是:0 (无限制) to 2147483647 (2GB)

  3. Django中静态目录的设置

    一般产品发布在Apache的环境下,Django静态目录的设置也在Apache中配置(这时 候Djangosettings.py中设置的静态目录配置将不起作用)。建议配置Apache时, 如果产品中没有执行使用类似phpasp脚本文件,Apache不要配置php,asp等的解 析模块,以防恶意用户利用公共目录执行非法的脚本文件。

重点检查:

  1. 上传文件的名称进行重命名;
  2. 限制上传文件的类型;
  3. 限制上传文件的大小;
  4. 建议在Apache配置文件中设置LimitRequestBody的值,严格限制上传文件的大小
  5. 如果产品中没有执行使用phpasp等脚本文件,建议Apache不要配置phpasp等的解析模块;

错误信息

产品发布后,要避免暴漏给用户任何错误信息,Django有简单的标记来控制这些错误的 显示。

Django项目的settings.py文件中:

  1. 关闭 Debug 模式:DEBUG = False

    如果为True,所有的数据库查询将被保存在内存中;任何404错误都将呈现 django的特殊的404页面;程序中任何未捕获的异常,从基本的python语法错误到 数据库错误以及模板语法错误,都将返回详细的错误信息。

  2. 关闭模板 Debug 模式:TEMPLATE_DEBUG = False

    如果为True,为了在错误页面上显示足够的东西,Django的模版系统会为每一 个模版保存一些额外的信息。

  3. 关闭Apache配置文件中的python调试选项

    确保任何在Django有机会载入之前发生的错误都将不会显示给公众。

重点检查:

  • Django项目的settings.py中将DEBUGTEMPLATE_DEBUG的值设为False
  • 关闭Apache配置文件中的python调试选项;
  • 总之,关闭其它所有Debug 模式。

目录穿越

目录穿越是另一种注入风格的攻击,恶意用户通过构建代码,欺骗文件系统来读或写WEB 服务器不允许访问的文件。

1
2
3
4
def dump_file(request):
filename = request. GET["filename"]
filename = os.path.join(BASE_PATH, filename)
content = open(filename).read()

尽管它看起来限制了文件访问BASE_PATH(通过使用os.path.join)下面的文件,如果攻击者传递一个包含”..”(这两个句点是UNIX对”父目录”的捷径)的filename,他就可以访问 BASE_PATH 之上的文件,比如../../../../../etc/passwd

如果程序是基于用户的输入来读写文件,要对用户的请求路径进行严格验证和过滤,来 确保攻击者不能从限制访问的基本目录逃离。

对于目录穿越的防御,过滤通常采用以下2种过滤方式:

  1. 使用”basename“函数过滤:

    1
    2
    3
    4
    def dump_file(request):
    filename = basename(request.GET["filename"])
    filename = os.path.join(BASE_PATH, filename)
    content = open(filename).read()
  2. 过滤文件名中所有的”..“:

    1
    2
    3
    4
    5
    def dump_file(request):
    filename = request.GET["filename"]
    filename = filename.replace("..","")
    filename = os.path.join(BASE_PATH, filename)
    content = open(filename).read()

设置权限

  1. 文件权限
    • 去掉WEB用户对常用、重要系统命令的读写执行权限;
    • 去掉其它用户对apache日志的读权限;
    • 去掉WEB上传目录其它脚本语言的解析权限;
  2. 数据库权限
    • 给用户授予其所需要的最小权限;
    • 取消默认账户不需要的权限;

Sameorigin(同源)

简单的讲,同源就是要求域名,协议,端口三者都一致。

先了解一下域名,比如 https://www.diandian100.cn:

diandian100.cn 主域名

www.diandian100.cn二级域名

img.diandian100.cn 二级域名

(www.diandian100.cn、img.diandian100.cn)虽然属于同一主域,但是子域不同,所以它们非同源。