提问人:Rune Grimstad 提问时间:5/26/2009 最后编辑:CommunityRune Grimstad 更新时间:2/14/2017 访问量:31993
避免不带参数的 SQL 注入
Avoiding SQL injection without parameters
问:
我们在这里进行了另一个关于在代码中使用参数化 sql 查询的讨论。在讨论中,我们有两个方面:我和其他一些人说我们应该始终使用参数来防止SQL注入,而其他人则认为没有必要。相反,他们希望在所有字符串中用两个撇号替换单个撇号,以避免 sql 注入。我们的数据库都运行 Sql Server 2005 或 2008,我们的代码库在 .NET Framework 2.0 上运行。
让我用 C# 举一个简单的例子:
我希望我们使用它:
string sql = "SELECT * FROM Users WHERE Name=@name";
SqlCommand getUser = new SqlCommand(sql, connection);
getUser.Parameters.AddWithValue("@name", userName);
//... blabla - do something here, this is safe
虽然其他人想这样做:
string sql = "SELECT * FROM Users WHERE Name=" + SafeDBString(name);
SqlCommand getUser = new SqlCommand(sql, connection);
//... blabla - are we safe now?
其中 SafeDBString 函数定义如下:
string SafeDBString(string inputValue)
{
return "'" + inputValue.Replace("'", "''") + "'";
}
现在,只要我们对查询中的所有字符串值使用 SafeDBString,我们就应该是安全的。右?
使用 SafeDBString 函数有两个原因。首先,这是自石头时代以来一直采用的方式,其次,调试 sql 语句更容易,因为您会看到在数据库上运行的 excact 查询。
那么。我的问题是,使用SafeDBString函数来避免sql注入攻击是否真的足够。我一直在尝试找到违反此安全措施的代码示例,但我找不到任何示例。
有没有人可以打破这个?你会怎么做?
编辑:总结一下到目前为止的回复:
- 目前还没有人找到绕过 Sql Server 2005 或 2008 上的 SafeDBString 的方法。这很好,我想?
- 一些回复指出,使用参数化查询时,您可以获得性能提升。原因是查询计划可以重用。
- 我们也同意,使用参数化查询可以提供更易于维护的可读性更强的代码
- 此外,始终使用参数比使用各种版本的 SafeDBString、字符串到数字的转换和字符串到日期的转换更容易。
- 使用参数,您可以获得自动类型转换,这在我们处理日期或十进制数时特别有用。
- 最后:不要像JulianR所写的那样试图自己做安全。数据库供应商在安全性上花费了大量的时间和金钱。我们不可能做得更好,也没有理由去努力做他们的工作。
因此,虽然没有人能够破坏 SafeDBString 函数的简单安全性,但我得到了许多其他很好的论据。谢谢!
答:
我会对所有事情都使用存储过程或函数,这样就不会出现问题。
在我必须将 SQL 放入代码中的地方,我使用参数,这是唯一有意义的事情。提醒持不同政见者,有些黑客比他们更聪明,并且有更好的动机来破解试图智取他们的代码。使用参数,这根本不可能,而且也不难。
评论
我使用了这两种方法来避免SQL注入攻击,并且绝对更喜欢参数化查询。当我使用串联查询时,我使用库函数来转义变量(如mysql_real_escape_string),并且不确定我已经在专有实现中涵盖了所有内容(似乎您也是)。
评论
然后有人去用“而不是”。国际海事组织(IMO)说,参数是唯一安全的方法。
它还避免了许多日期/数字的 i18n 问题;03/02/01 是几点?123,456多少钱?您的服务器(app-server 和 db-server)是否相互同意?
如果风险因素对他们来说没有说服力,那么性能如何?如果使用参数,RDBMS 可以重用查询计划,从而提高性能。它不能只用字符串来做到这一点。
评论
首先,“替换”版本的示例是错误的。您需要在文本周围加上撇号:
string sql = "SELECT * FROM Users WHERE Name='" + SafeDBString(name) & "'";
SqlCommand getUser = new SqlCommand(sql, connection);
因此,这是参数为您做的另一件事:您无需担心是否需要将值括在引号中。当然,你可以把它内置到函数中,但随后你需要给函数增加很多复杂性:如何知道作为 null 的 'NULL' 和作为字符串的 'NULL' 之间的区别,或者数字和恰好包含大量数字的字符串之间的区别。这只是错误的另一个来源。
另一件事是性能:参数化查询计划通常比串联计划缓存得更好,因此在运行查询时可能会为服务器节省一个步骤。
此外,转义单引号是不够的。许多数据库产品允许使用其他方法来转义攻击者可能利用的字符。例如,在MySQL中,您还可以使用反斜杠转义单引号。因此,下面的“name”值将仅使用函数炸毁MySQL,因为当您将单引号加倍时,第一个引号仍然被反斜杠转义,使第二个引号“处于活动状态”:SafeDBString()
x\' 或 1=1;--
此外,JulianR 在下面提出了一个很好的观点:永远不要尝试自己做安全工作。即使经过彻底的测试,也很容易以看似有效的微妙方式使安全编程出错。然后时间过去了,一年后,你发现你的系统在六个月前被破解了,直到那时你才知道。
始终尽可能依赖为您的平台提供的安全库。它们将由以安全代码为生的人编写,经过比您可以管理的要好得多的测试,如果发现漏洞,则由供应商提供服务。
评论
通过参数化查询,您可以获得的不仅仅是针对 SQL 注入的保护。您还可以获得更好的执行计划缓存潜力。如果使用 sql server 查询探查器,您仍然可以看到“在数据库上运行的确切 sql”,因此在调试 sql 语句方面也不会真正丢失任何内容。
评论
如果不使用参数,则无法轻松对用户输入进行任何类型检查。
如果使用 SQLCommand 和 SQLParameter 类进行数据库调用,则仍可以看到正在执行的 SQL 查询。查看 SQLCommand 的 CommandText 属性。
当参数化查询如此易于使用时,我总是对防止 SQL 注入的自行滚动方法持怀疑态度。其次,仅仅因为“一直都是这样做的”并不意味着这是正确的方法。
从我不得不调查SQL注入问题的很短的时间内,我可以看到,使值“安全”也意味着你正在关闭你可能真正想要在数据中使用撇号的情况的大门 - 某人的名字呢,例如O'Reilly。
这样就剩下参数和存储过程了。
是的,你应该始终尝试以你现在所知道的最好的方式实现代码,而不仅仅是它一直以来是如何完成的。
评论
''
'
O'Reilly
只有当你保证要传入一个字符串时,这才是安全的。
如果你在某个时候没有传入字符串怎么办?如果你只传递一个数字怎么办?
http://www.mywebsite.com/profile/?id=7;DROP DATABASE DB
最终将变成:
SELECT * FROM DB WHERE Id = 7;DROP DATABASE DB
评论
这里有几篇文章,你可能会发现它们有助于说服你的同事。
http://www.sommarskog.se/dynamic_sql.html
http://unixwiz.net/techtips/sql-injection.html
就我个人而言,我宁愿永远不允许任何动态代码接触我的数据库,要求所有联系都通过 sps(而不是使用动态 SQl 的)。这意味着我无法执行任何超出我授予用户权限的内容,并且内部用户(除了极少数出于管理目的具有生产访问权限的用户)无法直接访问我的表并造成破坏、窃取数据或进行欺诈。如果您运行金融应用程序,这是最安全的方法。
在安全问题上达成了一致。
使用参数的另一个原因是为了提高效率。
数据库将始终编译您的查询并缓存它,然后重用缓存的查询(对于后续请求来说,这显然更快)。 如果使用参数,则即使使用不同的参数,数据库也会在绑定参数之前根据 SQL 字符串进行匹配时重用缓存的查询。
但是,如果您不绑定参数,则 SQL 字符串会在每个请求(具有不同的参数)上更改,并且它永远不会与缓存中的内容匹配。
我认为正确答案是:
不要试图自己做安全。使用任何受信任的行业标准库来执行您要执行的操作,而不是尝试自己执行。无论您对安全性做出什么假设,都可能是不正确的。尽管你自己的方法看起来很安全(充其量看起来不稳定),但你有可能忽略一些东西,在安全性方面,你真的想抓住这个机会吗?
使用参数。
评论
以下是使用参数化查询的几个原因:
- 安全性 - 数据库访问层知道如何删除或转义数据中不允许的项目。
- 关注点分离 - 我的代码不负责将数据转换为数据库喜欢的格式。
- 没有冗余 - 我不需要在每个执行此数据库格式化/转义的项目中都包含程序集或类;它内置于类库中。
这个论点是没有赢家的。如果您确实设法发现了一个漏洞,您的同事将更改 SafeDBString 函数来解释它,然后要求您再次证明它是不安全的。
鉴于参数化查询是无可争议的编程最佳实践,举证责任应该由他们来说明为什么他们没有使用更安全、性能更好的方法。
如果问题是重写所有遗留代码,那么简单的折衷方案是在所有新代码中使用参数化查询,并重构旧代码以在处理该代码时使用它们。
我的猜测是,真正的问题是骄傲和固执,你对此无能为力。
很少有漏洞(我不记得是哪个数据库)与SQL语句的缓冲区溢出有关。
我想说的是,SQL-Injection 不仅仅是“逃避引号”,你不知道接下来会发生什么。
我没有看到任何其他回答者解决“为什么自己做不好”的这一方面,但考虑 SQL 截断攻击。
如果无法说服他们使用参数,还有 T-SQL 函数可能会有所帮助。它抓住了很多(所有?)逃脱的 qoute 问题。QUOTENAME
它可以被破坏,但方法取决于确切的版本/补丁等。
已经提出的一个是可以利用的溢出/截断错误。
另一个未来的方法是找到类似于其他数据库的错误 - 例如,MySQL/PHP堆栈遇到了转义问题,因为某些UTF8序列可用于操作替换函数 - 替换函数将被诱骗引入注入字符。
归根结底,替换安全机制依赖于预期但不是预期的功能。由于该功能不是代码的预期目的,因此很有可能某些发现的怪癖会破坏您的预期功能。
如果您有很多遗留代码,则可以将 replace 方法用作权宜之计,以避免冗长的重写和测试。如果你正在编写新代码,没有任何借口。
所以我会说:
1) 你为什么要尝试重新实现内置的东西?它就在那里,随时可用,易于使用,并且已经在全球范围内进行了调试。如果在其中发现未来的错误,它们将很快得到修复并可供所有人使用,而无需您执行任何操作。
2) 有哪些流程可以保证您不会错过对 SafeDBString 的调用?仅仅在一个地方错过它可能会引发一大堆问题。你会在多大程度上关注这些事情,并考虑当公认的正确答案如此容易获得时,这种努力浪费了多少。
3)您有多确定您已经覆盖了Microsoft(数据库和访问库的作者)在SafeDBString实现中知道的每个攻击媒介...
4)读取sql的结构有多容易?该示例使用 + 串联,参数非常类似于字符串。格式,更具可读性。
此外,还有 2 种方法可以计算出实际运行的内容 - 滚动您自己的 LogCommand 函数,一个没有安全问题的简单函数,或者甚至查看 sql 跟踪以计算出数据库认为真正发生的事情。
我们的 LogCommand 函数很简单:
string LogCommand(SqlCommand cmd)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(cmd.CommandText);
foreach (SqlParameter param in cmd.Parameters)
{
sb.Append(param.ToString());
sb.Append(" = \"");
sb.Append(param.Value.ToString());
sb.AppendLine("\"");
}
return sb.ToString();
}
无论对错,它都能为我们提供所需的信息,而不会出现安全问题。
评论
另一个重要的考虑因素是跟踪转义和未转义的数据。有大量的应用程序,无论是 Web 还是其他应用程序,似乎都无法正确跟踪数据何时是原始 Unicode、&-编码、格式化 HTML 等。很明显,跟踪哪些字符串是 – 编码的,哪些不是。''
当你最终改变某个变量的类型时,这也是一个问题——也许它曾经是一个整数,但现在它是一个字符串。现在你有一个问题。
由于已经给出的原因,参数是一个非常好的主意。但是我们讨厌使用它们,因为创建参数并将其名称分配给变量以供以后在查询中使用是三重间接头的破坏。
下面的类包装通常用于生成 SQL 请求的字符串生成器。它允许您编写参数化查询,而无需创建参数,因此您可以专注于 SQL。您的代码将如下所示...
var bldr = new SqlBuilder( myCommand );
bldr.Append("SELECT * FROM CUSTOMERS WHERE ID = ").Value(myId, SqlDbType.Int);
//or
bldr.Append("SELECT * FROM CUSTOMERS WHERE NAME LIKE ").FuzzyValue(myName, SqlDbType.NVarChar);
myCommand.CommandText = bldr.ToString();
我希望你同意,代码的可读性得到了很大的提高,输出是一个适当的参数化查询。
该类如下所示...
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
namespace myNamespace
{
/// <summary>
/// Pour le confort et le bonheur, cette classe remplace StringBuilder pour la construction
/// des requêtes SQL, avec l'avantage qu'elle gère la création des paramètres via la méthode
/// Value().
/// </summary>
public class SqlBuilder
{
private StringBuilder _rq;
private SqlCommand _cmd;
private int _seq;
public SqlBuilder(SqlCommand cmd)
{
_rq = new StringBuilder();
_cmd = cmd;
_seq = 0;
}
//Les autres surcharges de StringBuilder peuvent être implémenté ici de la même façon, au besoin.
public SqlBuilder Append(String str)
{
_rq.Append(str);
return this;
}
/// <summary>
/// Ajoute une valeur runtime à la requête, via un paramètre.
/// </summary>
/// <param name="value">La valeur à renseigner dans la requête</param>
/// <param name="type">Le DBType à utiliser pour la création du paramètre. Se référer au type de la colonne cible.</param>
public SqlBuilder Value(Object value, SqlDbType type)
{
//get param name
string paramName = "@SqlBuilderParam" + _seq++;
//append condition to query
_rq.Append(paramName);
_cmd.Parameters.Add(paramName, type).Value = value;
return this;
}
public SqlBuilder FuzzyValue(Object value, SqlDbType type)
{
//get param name
string paramName = "@SqlBuilderParam" + _seq++;
//append condition to query
_rq.Append("'%' + " + paramName + " + '%'");
_cmd.Parameters.Add(paramName, type).Value = value;
return this;
}
public override string ToString()
{
return _rq.ToString();
}
}
}
尽可能使用参数化查询。有时,即使是没有使用任何奇怪字符的简单输入,如果未将其标识为数据库中某个字段的输入,也已经可以创建 SQL 注入。
因此,只需让数据库完成其识别输入本身的工作,更不用说当您需要实际插入奇怪的字符时,它还可以省去很多麻烦,否则这些字符将被转义或更改。它甚至可以节省一些宝贵的运行时间,最终不必计算输入。
2年后,我又犯了......任何发现参数很痛苦的人都欢迎尝试我的 VS 扩展 QueryFirst。在实际的 .sql 文件(验证、智能感知)中编辑请求。要添加参数,只需将其直接键入 SQL,以“@”开头。保存文件时,QueryFirst 将生成包装类,以便运行查询并访问结果。它将查找参数的数据库类型并将其映射到 .net 类型,您将找到该类型作为生成的 Execute() 方法的输入。再简单不过了。以正确的方式做这件事比做任何其他方式都更快、更容易,而且创建 sql 注入漏洞变得不可能,或者至少是异常困难的。还有其他杀手锏,例如能够删除数据库中的列并立即查看应用程序中的编译错误。
法律免责声明:我写了 QueryFirst
评论