本文由Django作者之一的Jacob Kaplan-Moss以及Grayson Hardaway共同创作。
什么是SQL注入?
SQL注入(SQLi)是最危险的WEB漏洞之一。所幸这种情况渐少发生,主要归功于越来越多的开发者开始使用数据库抽象层(如Django的ORM)。然而,一旦经受此类攻击,后果将不堪设想。
当代码里错误的构造了包含用户输入的SQL查询时,就会发生SQLi。例如,假设在不了解SQLi的情况下,实现一个查询方法:
发现问题了吗?注意查询请求来自浏览器:request.GET['q']。思考一下如果请求包含单引号,在构建SQL时会发生什么呢?
假如攻击者查询' OR 'a'='a,这时SQL语句将变成:
如此就糟了,此查询会返回数据库表中的全部内容,导致数据泄露甚至数据库服务瘫痪。
还有更糟的,假如攻击者查询';DELETE FROM some_table。此时SQL语句如下:
哇呜,刺激!
防止SQLi的基本概念
我们将很快介绍Django的详细内容,不过在此之前,真正理解防止SQL注入的基本规则很重要:
永远不要信任用户提交的任何数据;
直接构造SQL查询时,请始终使用“参数化查询”。
来自用户的任意内容都有可能被恶意构建。即使看似安全的内容,像浏览器headers(例如Django中用户代理request.META['HTTP_USER_AGENT']),也很容易直接在浏览器或Burp,Charles这类工具中篡改。
实际上,在Django中,HttpRequest object中的内容作为传递给视图函数的第一个参数,都需要注意。尽管存在一些例外情况,但最好将request中的任何内容都视为不可信。
然而,对于那些不经过request传递的数据,并不意味着值得信任。例如下面这个例子:
试想,如果image.caption是由用户输入的......那可能就大事不妙了。因此,需要关注第二条原则:始终使用参数化查询。
参数化查询是指在SQL查询时,将所需数值以动态参数形式传入的一种机制。这些参数要么由数据库直接解释,要么经过安全转义后再添加到查询中。几乎所有的数据库客户端都支持参数化查询,如果您的不支持,还请换一个吧。
用参数化查询实现上面的查询方法如下所示:
注意SQL语句中的?,以及execute中的第二个参数,它是一个参数列表,其中的元素会注入查询语句以代替问号。
根据PEP-249,Python数据库API规范要求参数化查询,尽管不同的库可能对占位符使用不同的语法(如%样式参数、:named参数、数字参数等)。
您可以使用代码分析工具来检查SQL注入,Bento就是其中的一种,它可以捕获常见的SQL注入问题。不过,通过参数化查询来完全阻止此类攻击才是最佳方案。
Django如何防止SQLi
参数化查询在Django的ORM中无处不在,因此对于SQLi具有很强的防御能力。如果您的APP使用此框架进行数据库查询,那就相当安全了。
但是,在一些情况下,您还是得注意注入攻击,极少数的API并非100%安全,这些是进行自动化代码分析时应当重点关注的对象。
Raw Queries
有时ORM不足以满足需求,需要使用原生SQL。不过在此之前,还请考虑是否有避免这种情况的方法。例如,在数据库视图之上构建Django model,或者调用存储过程(stored procedure)都是行之有效的方案。
当然某些情况下,无法避免原生SQL,可以通过一些API来实现,不过不够安全。如下是Django提供的API:
1.Raw queries,例如:
2.RawSQL注解,例如:
3.直接使用数据库游标,例如:
4.避免:Queryset.extra(没有示例:此方法不安全,出于完整性考虑才将其包括在内)。
如何安全地使用这些API:
阅读本文第一部分,并确保了解参数化查询;
不要使用extra。此方法很难以100%安全的方式使用(即使不是不可能),因此视为弃用;
始终传递参数化的语句,即使参数列表为空,如下所示:
这是为了提醒您以后将参数添加到此列表,并让Bento之类的自动化工具更易发现潜在的API错误使用情况。
查询本身应始终是一个静态字符串,而不是由其他字符串处理而成。同样,也是为了让自动化工具更易发现API错误使用的情况。
自动预防
一种推荐的方案是通过代码分析工具来捕捉可预防的错误。俗话说,犯错者为人。Bento将自动检查Django代码的SQL注入模式。以下内容将检查代码库是否因为请求对象而导致SQL注入:
比起检查当前代码,更好的做法是检查未来的代码。Bento旨在作为一个预提交hook或在连续集成(CI)环境中运行。它可以识别差异,并且仅检查提交,从而既保证代码安全又确保快速的工作流程。如果您在项目中初始化Bento,将自动设置为检查提交。
这种基于提交的工作流在确保某些模式永远不会进入代码库方面特别强大。为了消除SQL注入,您应该自动检测代码:
始终使用参数化查询;
永远不用.extra。
Bento可以通过其他注册器来检测这些模式:
即使没有漏洞,这组规则也会高亮检测结果。如果一次性检测全部代码,会更严格。您可以使用Bento来归档,此时结果将被隐藏,直到准备好要处理它们。这样就可以继续检查代码中的问题,不会因为之前的结果而应接不暇。
Bento是基于Semgrep运行的。这是一种检测代码库bug和反模式的工具,它结合了grep在语法和语义搜索上的正确性。与普通grep相比,Semgrep的优势在于不受边界限制。
假设要检测如下SQL注入:
在Semgrep中可以这样表示:
在基于提交的工作流程中进行代码检测非常重要,因为可以从代码库中消除此类SQL注入。详情请参考https://sgrep.live/0X5。
其他ORMs
最后,如果您觉得Django的ORM还不能满足需求,可以采用SQLAlchemy来代替。这样您将失去许多Django的便捷功能,例如管理、模型表单以及基于模型的通用视图,但会获得更强大、表达性更好且足够安全的API。
自定义ORM
最后,即使您不使用原生SQL,也潜在一些风险。Django允许创建自定义聚合和自定义表达式,例如第三方库提供Document.objects.filter(title__similar_to=other_title)这样的API也能运行。
Django的核心ORM——核心的表达式、注解以及聚合——都已经相当成熟,出现SQLi的概率非常低。但是一些自定义的ORM,尤其是您自己实现的内容,还是存在一定的风险。
为了减轻这些高级功能带来的SQL注入风险,我建议以下几点:
首先,请谨慎引入第三方应用中自定义的表达式/聚合。在使用前,您应该仔细审核这些应用。该应用是否成熟、稳定且有人维护?是否有信心任何安全问题都能及时得到解决?当然,请防止在没有确认的情况下安装更新或是安全性较低的版本。
同样,在编写自己的自定义聚合时也要谨慎。请仔细阅读本文开头,以及有关避免在自定义表达式中进行SQL注入的Django文档。如文档所示,如果可以,应尽量避免在自定义表达式中进行字符串插入。如果不能,则需要自己转义所有表达式参数。这很难做到,且依赖数据库引擎和Python API的具体情况。谨慎入坑!
Bento的django.security.audit注册器可以检查代码库中是否有自定义ORM,还能以此快速审核第三方应用。这个开发条件有细微的差别,如果您在项目中发现此问题,请务必咨询这方面的专家!
总结
Django旨在防御SQL注入(和其他常见网络漏洞),其中最常用的内容都被自动保护,因此Django应用很少出现SQLi漏洞。
然而一旦发生,SQLi漏洞将是毁灭性的打击,因此值得花些时间来检查代码库以确保安全。Bento可以标记一些常见漏洞,既如此,就可以更好地编写安全的代码了。
英文原文:https://blog.r2c.dev/2020/preventing-sql-injection-a-django-authors-perspective/