本文参考自owasp,重点是提供清晰,简单,可操做的指导,以防止应用程序中的SQL注入漏洞。不幸的是,SQL注入攻击很常见,这是因为两个因素:php
发生了如此多的成功SQL注入攻击有点可耻,由于在代码中避免SQL注入漏洞很是简单。html
当软件开发人员建立包含用户提供的输入的动态数据库查询时,会引入SQL注入漏洞。为了不SQL注入缺陷很简单。开发人员须要:java
a)中止编写动态查询; mysql
b)防止用户提供的包含恶意SQL的输入影响所执行查询的逻辑。git
本文提供了一组经过避免这两个问题来防止SQL注入漏洞的简单技术。这些技术几乎能够与任何类型的数据库一块儿使用。还有其余类型的数据库,如XML数据库,可能有相似的问题(例如,XPath和XQuery注入),这些技术也可用于保护它们。github
主要防护:web
额外防护:sql
不安全的例子:数据库
SQL注入漏洞一般以下所示:编程
如下(Java)示例是UNSAFE,并容许攻击者将代码注入将由数据库执行的查询中。简单地附加到查询的未经验证的“customerName”参数容许攻击者注入他们想要的任何SQL代码。不幸的是,这种访问数据库的方法太常见了。
String query = "SELECT account_balance FROM user_data WHERE user_name = " + request.getParameter("customerName"); try { Statement statement = connection.createStatement( ... ); ResultSet results = statement.executeQuery( query ); } ...
使用带有变量绑定的预准备语句(也就是参数化查询)是全部开发人员应该首先学习如何编写数据库查询的方法。它们比动态查询更易于编写,更易于理解。参数化查询强制开发人员首先定义全部SQL代码,而后将每一个参数传递给查询。这种编码风格容许数据库区分代码和数据,不管提供什么用户输入。
准备好的语句可确保攻击者没法更改查询的意图,即便攻击者插入了SQL命令也是如此。在下面的安全示例中,若是攻击者输入的是userID tom' or '1'='1
,则参数化查询不会受到攻击,而是会查找与字符串彻底匹配的用户名tom' or '1'='1
。
特定语言的建议:
PreparedStatement()
与绑定变量一块儿使用SqlCommand()
或OleDbCommand()
使用绑定变量createQuery()
与绑定变量一块儿使用(在Hibernate中称为命名参数)sqlite3_prepare()
建立语句对象在极少数状况下,准备好的陈述会损害绩效。遇到这种状况时,最好是a)强烈验证全部数据或b)使用特定于数据库供应商的转义例程来转义全部用户提供的输入,以下所述,而不是使用预准备语句。
安全JavaSQL语句示例
如下代码示例使用PreparedStatement
Java的参数化查询实现来执行相同的数据库查询。
// 必定要验证 String custname = request.getParameter("customerName"); String query = "SELECT account_balance FROM user_data WHERE user_name = ? "; PreparedStatement pstmt = connection.prepareStatement( query ); pstmt.setString( 1, custname); ResultSet results = pstmt.executeQuery( );
安全C#.NET SQL语句示例
使用.NET,它更加直接。查询的建立和执行不会更改。您所要作的就是使用Parameters.Add()
此处所示的调用将参数传递给查询。
String query = "SELECT account_balance FROM user_data WHERE user_name = ?"; try { OleDbCommand command = new OleDbCommand(query, connection); command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text)); OleDbDataReader reader = command.ExecuteReader(); // … } catch (OleDbException se) { // error handling }
咱们已经在Java和.NET中展现了示例,但实际上全部其余语言(包括Cold Fusion和Classic ASP)都支持参数化查询接口。甚至SQL抽象层,如Hibernate查询语言(HQL)也有相同类型的注入问题(咱们称之为HQL注入)。HQL也支持参数化查询,所以咱们能够避免这个问题:
Hibernate查询语言(HQL)准备语句(命名参数)示例
//First is an unsafe HQL Statement Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'"); //Here is a safe version of the same query using named parameters Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid"); safeHQLQuery.setParameter("productid", userSuppliedParameter);
开发人员倾向于喜欢Prepared Statement方法,由于全部SQL代码都保留在应用程序中。这使您的应用程序相对数据库独立。
SQL注入并不老是安全的存储过程。可是,某些标准存储过程编程结构与安全实现时使用参数化查询具备相同的效果,这是大多数存储过程语言的标准。
它们要求开发人员只使用自动参数化的参数构建SQL语句,除非开发人员在很大程度上超出了标准。预准备语句和存储过程之间的区别在于,存储过程的SQL代码已定义并存储在数据库自己中,而后从应用程序中调用。这两种技术在防止SQL注入方面具备相同的效果,所以您的组织应该选择哪一种方法对您最有意义。
注意:'安全实现'意味着存储过程不包含任何不安全的动态SQL生成。开发人员一般不会在存储过程当中生成动态SQL。可是,它能够作到,但应该避免。若是没法避免,则存储过程必须使用输入验证或本文所述的正确转义,以确保不能使用全部用户提供的存储过程输入将SQL代码注入动态生成的查询中。审计人员应始终在SQL Server存储过程当中查找sp_execute,execute或exec的用法。相似的审计指南对于其余供应商的相似功能是必要的。
在某些状况下,存储过程可能会增长风险。例如,MS SQL服务器上,你有3个主要的默认角色:db_datareader
,db_datawriter
和db_owner
。在存储过程开始使用以前,DBA会根据要求为webservice的用户提供db_datareader或db_datawriter权限。可是,存储过程须要执行权限,默认状况下该角色不可用。用户管理已集中在一些设置,但仅限于这3个角色,致使全部Web应用程序在db_owner权限下运行,所以存储过程能够正常工做。固然,这意味着若是服务器遭到破坏,攻击者拥有数据库的彻底权限,之前他们可能只具备读访问权限。
安全Java存储过程示例
如下代码示例使用CallableStatement
Java的存储过程接口实现来执行相同的数据库查询。该sp_getAccountBalance
存储过程将在数据库中被预先定义和执行相同的功能与上述定义的查询。
// This should REALLY be validated String custname = request.getParameter("customerName"); try { CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}"); cs.setString(1, custname); ResultSet results = cs.executeQuery(); // … result set handling } catch (SQLException se) { // … logging and error handling }
安全的VB .NET存储过程示例
如下代码示例使用SqlCommand
.NET的存储过程接口实现来执行相同的数据库查询。该sp_getAccountBalance
存储过程将在数据库中被预先定义和执行相同的功能与上述定义的查询。
Try Dim command As SqlCommand = new SqlCommand("sp_getAccountBalance", connection) command.CommandType = CommandType.StoredProcedure command.Parameters.Add(new SqlParameter("@CustomerName", CustomerName.Text)) Dim reader As SqlDataReader = command.ExecuteReader() '... Catch se As SqlException 'error handling End Try
SQL查询的各个部分不是使用绑定变量的合法位置,例如表或列的名称,以及排序顺序指示符(ASC或DESC)。在这种状况下,输入验证或查询从新设计是最合适的防护。对于表或列的名称,理想状况下,这些值来自代码,而不是来自用户参数。
可是,若是用户参数值用于使表名和列名不一样,则应将参数值映射到合法/预期的表或列名,以确保未经验证的用户输入不会在查询中结束。请注意,这是设计不佳的症状,若是时间容许,应考虑彻底重写。
如下是表名验证的示例。
String tableName; switch(PARAM): case "Value1": tableName = "fooTable"; break; case "Value2": tableName = "barTable"; break; ... default : throw new InputValidationException("unexpected value provided" + " for table name");
该tableName
而后能够直接附加到SQL查询,由于它是目前已知的是在此查询表名的法律和预期值之一。请记住,通用表验证功能可能会致使数据丢失,由于表名用于不指望它们的查询中。
对于像排序顺序这样简单的东西,最好将用户提供的输入转换为布尔值,而后使用该布尔值选择要附加到查询的安全值。这是动态查询建立中很是标准的需求。
例如:
public String someMethod(boolean sortOrder) { String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");` ...
任什么时候候用户输入均可以转换为非String,如日期,数字,布尔值,枚举类型等,而后将其附加到查询中,或用于选择要追加到查询的值,这能够确保它是这样作是安全的。
在全部状况下,也建议将输入验证做为辅助防护,即便使用绑定变量,如本文后面所述。有关如何实施强白名单输入验证的更多技术在输入验证备忘单中进行了描述。
当上述任何一种方法都不可行时,该技术仅应做为最后的手段使用。输入验证多是一个更好的选择,由于与其余防护相比,这种方法很脆弱,咱们不能保证它会在全部状况下阻止全部SQL注入。
此技术是在将用户输入放入查询以前将其转义。它的实如今数据库方面很是具体。一般只建议在实现输入验证时不会下降遗留代码的成本效益。应该使用参数化查询,存储过程或某种为您构建查询的对象关系映射器(ORM)来构建或重写从头开始构建的应用程序或须要低风险容忍度的应用程序。
这种技术就是这样的。每一个DBMS都支持一种或多种特定于某些查询的字符转义方案。若是您使用正在使用的数据库的正确转义方案转义全部用户提供的输入,则DBMS不会将该输入与开发人员编写的SQL代码混淆,从而避免任何可能的SQL注入漏洞。
要专门为数据库编码器查找javadoc,请单击Codec
左侧的类。有不少编解码器实现。两个特定于数据库的编解码器是OracleCodec
,和MySQLCodec
。
只需All Known Implementing Classes:
在Interface Codec页面顶部单击其名称便可。
目前,ESAPI目前拥有如下数据库编码器:
数据库编码器即将推出:
若是您的数据库编码器丢失,请告诉咱们。
若是您想构建本身的转义例程,如下是咱们为ESAPI编码器开发的每一个数据库的转义细节:
使用ESAPI数据库编解码器很是简单。Oracle示例以下所示:
ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );
所以,若是您在代码中生成了一个现有的动态查询,该查询将转到Oracle,以下所示:
String query = "SELECT user_id FROM user_data WHERE user_name = '" + req.getParameter("userID") + "' and user_password = '" + req.getParameter("pwd") +"'"; try { Statement statement = connection.createStatement( … ); ResultSet results = statement.executeQuery( query ); }
你会重写第一行看起来像这样:
Codec ORACLE_CODEC = new OracleCodec(); String query = "SELECT user_id FROM user_data WHERE user_name = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("userID")) + "' and user_password = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("pwd")) +"'";
不管输入是什么,它如今均可以安全地进行SQL注入。
为了得到最大的代码可读性,您还能够构建本身的代码OracleEncoder
:
Encoder oe = new OracleEncoder(); String query = "SELECT user_id FROM user_data WHERE user_name = '" + oe.encode( req.getParameter("userID")) + "' and user_password = '" + oe.encode( req.getParameter("pwd")) +"'";
使用这种类型的解决方案,您只须要将每一个用户提供的参数包装成一个ESAPI.encoder().encodeForOracle( )
调用或者您命名为调用的任何内容,您就能够完成。
该LIKE
关键字容许进行文本扫描搜索。在Oracle中,下划线_
字符仅匹配一个字符,而&符号%
用于匹配任何字符的零次或屡次出现。必须在LIKE子句条件中转义这些字符。
例如:
SELECT name FROM emp WHERE id LIKE '%/_%' ESCAPE '/';
SELECT name FROM emp WHERE id LIKE '%\%%' ESCAPE '\';
MySQL支持两种转义模式:
ANSI_QUOTES
SQL模式,以及这个关闭的模式,咱们称之为MySQL
模式。ANSI SQL
模式:'
使用''
(两个单一刻度)简单编码全部(单个刻度)字符
MySQL
模式,执行如下操做:
NUL (0x00) --> \0 [This is a zero, not the letter O] BS (0x08) --> \b TAB (0x09) --> \t LF (0x0a) --> \n CR (0x0d) --> \r SUB (0x1a) --> \Z " (0x22) --> \" % (0x25) --> \% ' (0x27) --> \' \ (0x5c) --> \\ _ (0x5f) --> \_ all other non-alphanumeric characters with ASCII values less than 256 --> \c where 'c' is the original non-alphanumeric character.
咱们尚未实现SQL Server转义例程,可是下面有很好的指针和连接到描述如何防止SQL服务器上的SQL注入攻击的文章,请参见此处。
此信息基于DB2 WebQuery特殊字符以及Oracle JDBC DB2驱动程序中的一些信息。
有关几个DB2 Universal驱动程序之间差别的信息。
转义的一个特殊状况是对从用户接收的整个字符串进行十六进制编码的过程(这能够看做是转义每一个字符)。Web应用程序应在将用户输入包含在SQL语句中以前对其进行十六进制编码。SQL语句应该考虑到这一事实,并相应地比较数据。
例如,若是咱们必须查找匹配sessionID的记录,而且用户将字符串abc123做为会话ID发送,则select语句将为:
SELECT ... FROM session WHERE hex_encode(sessionID) = '616263313233'
hex_encode
应该由所使用的数据库的特定工具替换。字符串606162313233是从用户接收的字符串的十六进制编码版本(它是用户数据的ASCII / UTF-8代码的十六进制值的序列)。
若是攻击者要传输包含单引号字符的字符串,而后尝试注入SQL代码,则构造的SQL语句将只显示以下:
... WHERE hex_encode ( ... ) = '2720 ... '
27
是单引号的ASCII代码(十六进制),它与字符串中的任何其余字符同样只是十六进制编码。产生的SQL只能包含数字数字和字母a
来f
,历来没有任何特殊字符,它可能会使SQL注入。
使用预准备语句和参数化查询。这些是由数据库服务器与任何参数分开发送和解析的SQL语句。这样攻击者就没法注入恶意SQL。
你基本上有两个选择来实现这个目标:
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute(array('name' => $name)); foreach ($stmt as $row) { // do something with $row }
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?'); $stmt->bind_param('s', $name); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // do something with $row }
PDO是通用选项。若是您要链接到MySQL之外的数据库,则能够引用特定于驱动程序的第二个选项(例如,对于PostgreSQL,请使用pg_prepare()和pg_execute())。
除了采用四种主要防护之一外,咱们还建议采用全部这些额外的防护措施,以便提供深度防护。这些额外的防护是:
为了最大限度地减小成功SQL注入攻击的潜在损害,您应该最小化分配给环境中每一个数据库账户的权限。不要为您的应用程序账户分配DBA或管理员类型访问权限。咱们知道这很容易,当你这样作时,一切都“有效”,但这是很是危险的。
从头开始肯定您的应用程序账户须要哪些访问权限,而不是试图找出您须要带走的访问权限。确保仅须要读访问权限的账户才被授予对他们须要访问的表的读访问权限。
若是账户只须要访问表的某些部分,请考虑建立一个视图,以限制对该部分数据的访问,并为账户分配账户访问权限,而不是基础表。不多,若是有的话,授予对数据库账户的建立或删除访问权限。
若是您采用的策略是在任何地方使用存储过程,而且不容许应用程序账户直接执行本身的查询,那么将这些账户限制为只能执行所需的存储过程。不要直接向数据库中的表授予任何权限。
SQL注入不是对数据库数据的惟一威胁。攻击者能够简单地将参数值从它们所呈现的合法值之一更改成未经受权的值,但应用程序自己可能被受权访问。所以,尽可能减小授予应用程序的权限将下降此类未经受权的访问尝试的可能性,即便攻击者没有尝试将SQL注入用做其漏洞利用的一部分。
在您使用它时,您应该最小化DBMS运行的操做系统账户的权限。不要以root用户身份或系统运行DBMS!大多数DBMS都是开箱即用的,具备很是强大的系统账户。例如,默认状况下,MySQL在Windows上做为系统运行!使用受限制的权限将DBMS的OS账户更改成更合适的账户。
Web应用程序的设计者不只应避免在Web应用程序中使用相同的全部者/管理员账户来链接到数据库。不一样的DB用户能够用于不一样的Web应用程序。
一般,须要访问数据库的每一个单独的Web应用程序均可以具备指定的数据库用户账户,Web应用程序将使用该账户链接到数据库。这样,应用程序的设计者能够在访问控制中具备良好的粒度,从而尽量地减小特权。而后,每一个数据库用户均可以选择访问它所需的内容,并根据须要进行写访问。
例如,登陆页面须要对表的用户名和密码字段进行读访问,但不能对任何表单进行写访问(无插入,更新或删除)。可是,注册页面固然须要对该表的插入权限; 只有当这些Web应用程序使用不一样的DB用户链接到数据库时,才能强制执行此限制。
经过限制对表的特定字段或表的链接的读访问,可使用SQL视图进一步增长访问的粒度。它可能具备额外的好处:例如,假设系统须要(可能因为某些特定的法律要求)来存储用户的密码,而不是盐渍的密码。
设计师可使用视图来弥补这种限制; 撤消对表的全部访问(来自除全部者/管理员以外的全部数据库用户)并建立一个输出密码字段的哈希而不是字段自己的视图。任何成功窃取数据库信息的SQL注入攻击都将被限制为窃取密码的哈希值(甚至多是键控哈希值),由于任何Web应用程序的数据库用户都无权访问表自己。