避免不带参数的SQL注入

时间:2009-05-26 12:38:21

标签: c# asp.net sql-server sql-injection

我们正在讨论在我们的代码中使用参数化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语句。

那么。我的问题是使用SafeDBString函数是否真的足以避免sql注入攻击。我一直试图找到破坏这种安全措施的代码示例,但我找不到任何示例。

有没有人可以打破这个?你会怎么做?

修改 总结到目前为止的回复:

  • 还没有人找到解决Sql Server 2005或2008上的SafeDBString的方法。我认为这很好吗?
  • 一些回复指出,使用参数化查询时可以获得性能提升。原因是查询计划可以重复使用。
  • 我们也同意使用参数化查询提供更易于维护的可读代码
  • 此外,总是使用参数比使用各种版本的SafeDBString,字符串到数字转换和字符串到日期转换更容易。
  • 使用参数可以获得自动类型转换功能,这在我们处理日期或十进制数时特别有用。
  • 最后:Don't try to do security yourself正如朱利安写的那样。数据库供应商在安全性上花费了大量时间和金钱。我们没有办法做得更好,没有理由我们应该努力做好自己的工作。

因此,虽然没有人能够打破SafeDBString函数的简单安全性,但我得到了许多其他好的参数。谢谢!

21 个答案:

答案 0 :(得分:82)

我认为正确答案是:

不要试图自己做安全。使用任何可信的行业标准库,您可以尝试执行此操作,而不是尝试自行完成。无论您对安全做出什么假设,都可能是不正确的。尽管你自己的方法可能看起来很安全(而且它看起来最好看起来很不稳定),但是你有可能忽略某些东西并且你真的想在安全方面抓住这个机会吗?

使用参数。

答案 1 :(得分:71)

然后有人去使用“而不是”。参数是,IMO,唯一安全的方式。

它还避免了很多关于日期/数字的i18n问题;什么日期是01/02/03? 123,456多少钱?您的服务器(app-server和db-server)是否同意?

如果风险因素不能令他们信服,那么表现怎么样?如果使用参数,RDBMS可以重用查询计划,从而有助于提高性能。只有字符串才能做到这一点。

答案 2 :(得分:27)

这个论点是不赢的。如果您确实设法找到漏洞,您的同事将只更改SafeDBString函数来解释它,然后要求您再次证明它是不安全的。

鉴于参数化查询是无可争议的编程最佳实践,因此应该在他们身上证明他们为什么不使用既安全又更好的方法。

如果问题是重写所有遗留代码,那么容易妥协的是在所有新代码中使用参数化查询,并重构旧代码以在处理该代码时使用它们。

我的猜测是实际问题是骄傲和顽固,而且你可以做的更多。

答案 3 :(得分:19)

首先,您的“替换”版本的示例是错误的。你需要在文本周围放置撇号:

string sql = "SELECT * FROM Users WHERE Name='" + SafeDBString(name) & "'";
SqlCommand getUser = new SqlCommand(sql, connection);

这是参数为您做的另一件事:您不必担心值是否需要用引号括起来。当然,你可以在函数中构建它,但是你需要为函数添加很多复杂性:如何知道'NULL'和null之间的区别和'NULL'只是一个字符串,或者在数字和恰好包含大量数字的字符串。它只是bug的另一个来源。

另一件事是性能:参数化查询计划通常比连接计划更好地缓存,因此可能在运行查询时将服务器保存一步。

此外,转义单引号还不够好。许多数据库产品允许使用其他方法来转发攻击者可以利用的字符。例如,在MySQL中,您还可以使用反斜杠转义单引号。所以下面的“名称”值只会使用SafeDBString()函数炸毁MySQL,因为当你将单引号加倍时,第一个仍然被反斜杠转义,第二个被“激活”:

  

x \'或1 = 1; -


此外,JulianR在下面提出了一个很好的观点: 从不 尝试自己做安全工作。即使进行全面测试,也很容易以微妙的方式使安全编程错误地出现。然后时间流逝,一年后你发现你的系统在六个月前被破解了,直到那时你才知道它。

始终尽可能地依赖为您的平台提供的安全库。它们将由那些以安全代码为生的人编写,比您可以管理的更好测试,如果发现漏洞,则由供应商提供服务。

答案 4 :(得分:10)

所以我会说:

1)你为什么要重新实现内置的东西?它就在那里,随时可用,易于使用并已在全球范围内进行调试。如果在其中发现未来的错误,它们将被修复并且可以非常快速地提供给每个人,而无需您做任何事情。

2)保证的哪些流程从不错过了对SafeDBString的调用?在一个地方错过它可能会打开一大堆问题。你会对这些事情有多大关注,并考虑在接受到的正确答案如此容易达到的情况下,这种努力浪费了多少。

3)您是否确定已经覆盖了Microsoft(DB和访问库的作者)在您的SafeDBString实现中所知道的每个攻击媒介......

4)读取sql的结构有多容易?该示例使用+连接,参数非常类似于string.Format,它更具可读性。

此外,有两种方法可以解决实际运行的问题 - 滚动您自己的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();
    }

对或错,它为我们提供了所需的信息而没有安全问题。

答案 5 :(得分:7)

使用参数化查询,您可以获得的不仅仅是针对sql注入的保护。您还可以获得更好的执行计划缓存潜力。如果您使用sql server查询分析器,您仍然可以看到“在数据库上运行的确切sql”,因此您在调试sql语句方面也没有真正丢失任何内容。

答案 6 :(得分:5)

我已经使用这两种方法来避免SQL注入攻击,并且肯定更喜欢参数化查询。当我使用连接查询时,我使用库函数来转义变量(比如mysql_real_escape_string),并且不相信我已经涵盖了专有实现中的所有内容(因为它似乎也是如此)。

答案 7 :(得分:4)

如果不使用参数,您将无法轻松地对用户输入进行任何类型检查。

如果使用SQLCommand和SQLParameter类进行数据库调用,您仍然可以看到正在执行的SQL查询。查看SQLCommand的CommandText属性。

当参数化查询非常容易使用时,我总是怀疑自己采用滚动方法来防止SQL注入。其次,仅仅因为“它总是这样做”并不意味着它是正确的方法。

答案 8 :(得分:3)

如果您保证要传入字符串,这是唯一安全的。

如果你在某个时候没有传入一个字符串怎么办?如果只传递一个数字怎么办?

http://www.mywebsite.com/profile/?id=7;DROP DATABASE DB

最终会成为:

SELECT * FROM DB WHERE Id = 7;DROP DATABASE DB

答案 9 :(得分:2)

非常同意安全问题 使用参数的另一个原因是效率。

数据库将始终编译您的查询并对其进行缓存,然后重新使用缓存查询(这显然对后续请求更快)。 如果您使用参数,那么即使您使用不同的参数,数据库也会在绑定参数之前根据SQL字符串重新使用您的缓存查询。

如果你没有绑定参数,那么SQL字符串会在每个请求(具有不同参数)上发生变化,并且永远不会与缓存中的内容相匹配。

答案 10 :(得分:2)

我会为所有内容使用存储过程或函数,因此不会出现问题。

我必须将SQL放入代码中,我使用参数,这是唯一有意义的事情。提醒持不同政见者,有黑客比他们更聪明,并有更好的动机打破试图超越他们的代码。使用参数,这根本不可能,而且不是很困难。

答案 11 :(得分:1)

以下是一些文章,您可能会发现有助于说服您的同事。

http://www.sommarskog.se/dynamic_sql.html

http://unixwiz.net/techtips/sql-injection.html

我个人更喜欢永远不允许任何动态代码触及我的数据库,要求所有联系人都通过sps(而不是使用动态SQl的联系人)。这意味着没有任何事情可以解释我给用户的权限,并且内部用户(除了极少数具有管理目的的生产访问权限)不能直接访问我的表并创建破坏,窃取数据或提交欺诈。如果您运行财务应用程序,这是最安全的方法。

答案 12 :(得分:1)

从很短的时间起我就不得不调查SQL注入问题了,我可以看到,创造一个“安全”的价值也意味着你正在关闭你可能真正想要数据中的撇号的大门 - 什么关于某人的名字,例如O'Reilly。

留下参数和存储过程。

是的,您应该始终尝试以您现在知道的最佳方式实现代码 - 而不仅仅是如何完成代码。

答案 13 :(得分:1)

我没有看到任何其他的回答者解决'为什么自己做坏事'的这一方面,但考虑一个SQL Truncation attack

如果你无法说服他们使用params,那么还有QUOTENAME T-SQL函数会有所帮助。它抓住了许多(全部?)逃脱的qoute问题。

答案 14 :(得分:1)

它可以被破坏,但手段取决于确切的版本/补丁等。

已经提出的是可以利用的溢出/截断错误。

另一种未来的方法是找到类似于其他数据库的bug - 例如MySQL / PHP堆栈遇到了逃避问题,因为某些UTF8序列可以用来操作替换函数 - 替换函数会被欺骗进入引入注入字符。

在一天结束时,替换安全机制依赖于预期的但不依赖于预期的功能。由于功能不是代码的预期目的,因此很有可能一些发现的怪癖会破坏您的预期功能。

如果你有很多遗留代码,那么替换方法可以用作权宜之计,以避免冗长的重写和测试。如果您正在编写新代码,则没有任何理由。

答案 15 :(得分:1)

由于已经给出的原因,参数是一个非常好的主意。但我们讨厌使用它们,因为创建参数并将其名称分配给变量以供以后在查询中使用是一个三重间接头部残骸。

以下类包装了您将常用于构建SQL请求的stringbuilder。它允许您编写参数化查询而无需创建参数,因此您可以专注于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();
        }
    }
}

答案 16 :(得分:1)

始终尽可能使用参数化查询。有时,即使是没有使用任何奇怪字符的简单输入,如果未将其标识为数据库中字段的输入,也可以创建SQL注入。

因此,让数据库执行识别输入本身的工作,更不用说它还可以在您需要实际插入奇怪的字符时节省分配麻烦,否则这些字符将被转义或更改。它甚至可以节省一些有价值的运行时间,而不必计算输入。

答案 17 :(得分:1)

2 years later,我承认......任何发现参数痛苦的人都欢迎尝试我的VS扩展程序, QueryFirst 。您可以在真实的.sql文件中编辑您的请求(验证,智能感知)。要添加参数,只需将其直接输入到SQL中,从“@”开始。保存文件时,QueryFirst将生成包装类,以便您运行查询并访问结果。它将查找参数的DB类型并将其映射到.net类型,您将找到它作为生成的Execute()方法的输入。 不能简单。以正确的方式执行它比任何其他方式更快更容易,并且创建SQL注入漏洞变得不可能,或者至少是非常困难的。还有其他一些优势,例如能够删除数据库中的列并立即在应用程序中看到编译错误。

法律免责声明:我写过QueryFirst

答案 18 :(得分:0)

以下是使用参数化查询的几个原因:

  1. 安全性 - 数据库访问层知道如何删除或转义数据中不允许的项目。
  2. 关注点分离 - 我的代码不负责将数据转换为数据库喜欢的格式。
  3. 没有冗余 - 我不需要在每个执行此数据库格式化/转义的项目中包含程序集或类;它内置于类库中。

答案 19 :(得分:0)

与SQL语句的缓冲区溢出相关的漏洞很少(我不记得它是哪个数据库)。

我想说的是,SQL-Injection不仅仅是“逃避​​引用”,而且你不知道接下来会发生什么。

答案 20 :(得分:0)

另一个重要的考虑因素是跟踪转义和未转义的数据。有大量和大量的应用程序,Web和其他,似乎没有正确地跟踪数据是什么时候原始 - Unicode,&amp; -encoded,格式化的HTML,等等。很明显,很难跟踪哪些字符串是'' - 编码的,哪些不是。

当你最终改变某个变量的类型时,这也是一个问题 - 也许它曾经是一个整数,但现在它是一个字符串。现在你遇到了问题。