110

我们正在讨论在我们的代码中使用参数化的 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、字符串到数字的转换和字符串到日期的转换更容易。
  • 使用参数,您可以获得自动类型转换,这在我们处理日期或十进制数字时特别有用。
  • 最后:不要像 JulianR 所写的那样尝试自己做安全工作。数据库供应商在安全性上花费了大量时间和金钱。我们没有办法做得更好,也没有理由尝试做他们的工作。

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

4

21 回答 21

83

我认为正确的答案是:

不要试图自己做安全。使用任何值得信赖的行业标准库来解决您正在尝试做的事情,而不是尝试自己做。无论您对安全性做出何种假设,都可能是不正确的。尽管您自己的方法看起来很安全(而且充其量看起来很不稳定),但您可能会忽略某些东西,您真的想在安全方面抓住这个机会吗?

使用参数。

于 2009-05-26T13:31:01.507 回答
72

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

它还避免了很多关于日期/数字的 i18n 问题;03 年 1 月 2 日是几号?123,456 是多少?您的服务器(app-server 和 db-server)是否相互同意?

如果风险因素对他们没有说服力,那么绩效呢?如果您使用参数,RDBMS 可以重用查询计划,从而提高性能。它不能只用字符串来做到这一点。

于 2009-05-26T12:45:21.893 回答
27

争论是没有胜利的。如果您确实设法找到了一个漏洞,您的同事只会更改 SafeDBString 函数来解决它,然后要求您再次证明它是不安全的。

鉴于参数化查询是无可争议的编程最佳实践,他们应该承担举证责任,说明为什么他们没有使用更安全、性能更好的方法。

如果问题是重写所有遗留代码,那么简单的折衷方案是在所有新代码中使用参数化查询,并重构旧代码以在处理该代码时使用它们。

我的猜测是真正的问题是骄傲和固执,对此你无能为力。

于 2009-05-26T13:57:50.640 回答
19

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

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

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

另一件事是性能:参数化查询计划通常比级联计划缓存得更好,因此可能在运行查询时为服务器节省了一步。

此外,转义单引号还不够好。 许多数据库产品允许使用替代方法来转义攻击者可以利用的字符。例如,在 MySQL 中,您还可以使用反斜杠转义单引号。所以下面的“name”值只会用这个SafeDBString()函数来破坏 MySQL,因为当你把单引号加倍时,第一个引号仍然被反斜杠转义,而第二个是“活动的”:

x\' 或 1=1;--


此外,JulianR 在下面提出了一个很好的观点: 永远不要尝试自己做安全工作。即使经过彻底的测试,也很容易以看似有效的微妙方式使安全编程出错。然后时间过去了,一年后你发现你的系统在六个月前被破解了,直到那时你才知道。

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

于 2009-05-26T12:45:30.567 回答
10

所以我想说:

1)你为什么要重新实现一些内置的东西?它就在那里,随时可用,易于使用,并且已经在全球范围内进行了调试。如果在其中发现未来的错误,它们将很快得到修复并可供所有人使用,而无需您做任何事情。

2) 有哪些流程可以保证不会错过对 SafeDBString 的调用?仅仅在一个地方错过它可能会引发一大堆问题。您将如何关注这些事情,并考虑当接受的正确答案如此容易获得时,这些努力浪费了多少。

3) 你有多确定你已经掩盖了微软(数据库和访问库的作者)在你的 SafeDBString 实现中知道的每一个攻击向量......

4) 读取 sql 的结构有多容易?示例使用+连接,参数很像string.Format,更具可读性。

此外,有 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();
    }

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

于 2009-05-26T15:00:43.790 回答
7

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

于 2009-05-26T12:47:49.643 回答
5

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

于 2009-05-26T12:44:21.367 回答
4

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

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

当参数化查询如此易于使用时,我总是怀疑使用自己滚动的方法来防止 SQL 注入。其次,仅仅因为“它总是以这种方式完成”并不意味着它是正确的方式。

于 2009-05-26T12:58:58.870 回答
3

仅当您保证要传入字符串时,这才是安全的。

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

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

最终会变成:

SELECT * FROM DB WHERE Id = 7;DROP DATABASE DB
于 2009-05-26T13:17:18.477 回答
2

我会为所有事情使用存储过程或函数,所以不会出现问题。

在我必须将 SQL 放入代码的地方,我使用参数,这是唯一有意义的事情。提醒持不同政见者,有些黑客比他们更聪明,并且更有动力破解试图智取他们的代码。使用参数,根本不可能,也不难。

于 2009-05-26T12:43:45.593 回答
2

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

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

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

于 2009-05-26T13:26:53.497 回答
2

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

以下类包装了您通常用于构建 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();
        }
    }
}
于 2014-10-17T08:49:33.897 回答
1

在我不得不调查 SQL 注入问题的很短的时间内,我可以看到,使值“安全”也意味着您正在关闭可能实际需要在数据中使用撇号的情况的大门 - 那某人的名字呢,例如奥莱利。

剩下的就是参数和存储过程。

是的,你应该总是尝试以你现在知道的最好的方式来实现代码——而不仅仅是它一直是如何完成的。

于 2009-05-26T13:09:21.440 回答
1

这里有几篇文章,您可能会发现它们有助于说服您的同事。

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

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

就我个人而言,我更喜欢永远不允许任何动态代码接触我的数据库,要求所有联系都通过 sps(而不是使用动态 SQl 的联系)。这意味着除了我授予用户的权限之外,其他任何事情都无法完成,并且内部用户(除了极少数出于管理目的而具有生产访问权限的用户)无法直接访问我的表并造成破坏、窃取数据或进行欺诈。如果您运行财务应用程序,这是最安全的方法。

于 2009-05-26T13:20:29.140 回答
1

我没有看到任何其他答案解决了“为什么自己做不好”的这一方面,但考虑SQL 截断攻击

QUOTENAME如果您无法说服他们使用参数,还有一个T-SQL 函数会很有帮助。它捕获了很多(全部?)逃逸的 qoute 问题。

于 2009-05-26T14:49:12.690 回答
1

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

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

另一个未来的方法是寻找与其他数据库类似的错误——例如 MySQL/PHP 堆栈遇到了转义问题,因为某些 UTF8 序列可以用于操作替换函数——替换函数将被欺骗引入注入字符。

归根结底,替代安全机制依赖于预期但并非预期的功能。由于该功能不是代码的预期目的,因此某些发现的怪癖很可能会破坏您的预期功能。

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

于 2009-05-26T14:58:07.617 回答
1

尽可能使用参数化查询。有时,即使是一个没有使用任何奇怪字符的简单输入,如果它没有被识别为数据库中某个字段的输入,它就已经可以创建一个 SQL 注入。

因此,只需让数据库自己完成识别输入的工作,更不用说当您需要实际插入可能会被转义或更改的奇怪字符时,它还可以省去很多麻烦。它甚至可以最终节省一些宝贵的运行时间,而不必计算输入。

于 2014-10-23T09:27:53.903 回答
1

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

法律免责声明:我写了 QueryFirst

于 2016-09-28T12:54:37.197 回答
0

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

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

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

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

于 2009-05-26T14:38:50.497 回答
0

另一个重要的考虑因素是跟踪转义和未转义的数据。有大量的应用程序(Web 和其他应用程序)似乎无法正确跟踪数据何时为原始 Unicode、& 编码、格式化 HTML 等。很明显,要跟踪哪些字符串是''编码的,哪些不是。

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

于 2009-05-29T02:45:04.760 回答