避免不带参数的 SQL 注入

Avoiding SQL injection without parameters

提问人:Rune Grimstad 提问时间:5/26/2009 最后编辑:CommunityRune Grimstad 更新时间:2/14/2017 访问量:31993

问:

我们在这里进行了另一个关于在代码中使用参数化 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 函数的简单安全性,但我得到了许多其他很好的论据。谢谢!

C# asp.net SQL-Server SQL 注入

评论

16赞 annakata 5/26/2009
你的同事很远,很远,离基地很远。挑战他们找到一篇文献来支持他们的立场。新石座的论点是荒谬的,事情会发生变化,只有被困在石器时代的人才能适应。
1赞 Arjan Einbu 5/26/2009
好吧,至少你的同事可以防止不同形式的黑客攻击之一......他们确定所有参数化查询都这样做吗?(我不是...
1赞 Robert Gowland 5/26/2009
任何一个漏洞都无法说服他们。如果你带来了几个漏洞(这是你所要求的)和其他问题,并一一指出参数将解决这个问题,而你的团队将不得不编写大量的代码来提供一小部分功能,你可能会赢得他们的支持。祝你好运。
3赞 Bridge 2/28/2012
即使没有单引号,您仍然可以使用逻辑来破坏代码。尝试使用用户名“test OR 1=1” - 您将返回所有行,而不仅仅是带有用户名 test 的行!
1赞 jeroenh 9/28/2016
叹息。我真的不明白我们作为一个行业是如何设法容忍这种不专业的行为的。

答:

2赞 John Saunders 5/26/2009 #1

我会对所有事情都使用存储过程或函数,这样就不会出现问题。

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

评论

0赞 John Saunders 5/26/2009
好的,如何使用参数进行SQL注入?
0赞 Brian 5/26/2009
@Saunders:第 1 步是在数据库的参数处理功能中查找缓冲区溢出错误。
2赞 John Saunders 5/27/2009
找到一个了吗?在一个每天被数十万黑客攻击的商业数据库中?一个由一家财力雄厚的软件公司制造的?如果可能的话,你可以按名字引用诉讼。
1赞 Marc Gravell 5/27/2009
当然,如果 SPROC 使用串联和 EXEC(而不是 sp_ExecuteSQL),您又会遇到麻烦......(我已经看到它做错了太多次了,以至于无法打折......
5赞 RedBlueThing 5/26/2009 #2

我使用了这两种方法来避免SQL注入攻击,并且绝对更喜欢参数化查询。当我使用串联查询时,我使用库函数来转义变量(如mysql_real_escape_string),并且不确定我已经在专有实现中涵盖了所有内容(似乎您也是)。

评论

2赞 Bill Karwin 5/27/2009
+1,因为 mysql_real_escape_string() 转义了 \x00、\x1a、\n \r ' 和 ”。它还处理字符集问题。OP 的同事幼稚功能不会做任何这些!
72赞 Marc Gravell 5/26/2009 #3

然后有人去用“而不是”。国际海事组织(IMO)说,参数是唯一安全的方法。

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

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

评论

0赞 Rune Grimstad 5/26/2009
我已经尝试了格式和性能参数,但他们仍然不相信。
5赞 tnyfst 5/26/2009
实际上,无论您是否使用参数,sql server 都可以重用查询计划。我同意其他论点,但在大多数情况下,参数化 sql 的性能参数不再适用。
1赞 John Saunders 5/27/2009
@tnyfst:当每个参数值组合的查询字符串发生变化时,它可以重用执行计划吗?我认为这是不可能的。
4赞 marc_s 5/27/2009
如果查询文本与较早的查询文本相同,则将重用查询计划。因此,如果您发送两次完全相同的查询,它将被重用。但是,如果只更改空格或逗号或其他内容,则必须确定新的查询计划。
1赞 AnthonyWJones 5/28/2009
@Marc:我不确定你是否完全正确。SQL Server 缓存 hueristics 有点奇怪。解析器能够识别文本中的常量,并且可以人为地将 SQL 字符串转换为一个 uses 参数。然后,它可以将这个新的参数化查询的文本插入到缓存中。后续类似的 SQL 可能会在缓存中找到其参数化版本匹配。但是,参数化版本并不总是与缓存的原始 SQL 版本一起使用,我怀疑 SQL 有无数与性能相关的原因在这两种方法之间进行选择。
19赞 Joel Coehoorn 5/26/2009 #4

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

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

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

评论

5赞 Rune Grimstad 5/26/2009
replace 函数添加撇号
5赞 Joel Coehoorn 5/26/2009
那么它只是 bug 的又一个来源。它如何知道作为 null 值的 NULL 和作为文本字符串的 NULL 之间的区别?还是在数字输入和恰好包含数字的字符串之间?
0赞 Rune Grimstad 5/26/2009
好点子。您应该只将该函数用于字符串,也可能用于日期,因此您必须小心。这是使用参数的另一个原因!耶!
7赞 Steve Willcock 5/26/2009 #5

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

评论

0赞 Bill Karwin 5/27/2009
MySQL还记录参数化查询,其中插入了参数值。
4赞 Tim Scarborough 5/26/2009 #6

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

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

当参数化查询如此易于使用时,我总是对防止 SQL 注入的自行滚动方法持怀疑态度。其次,仅仅因为“一直都是这样做的”并不意味着这是正确的方法。

1赞 quamrana 5/26/2009 #7

从我不得不调查SQL注入问题的很短的时间内,我可以看到,使值“安全”也意味着你正在关闭你可能真正想要在数据中使用撇号的情况的大门 - 某人的名字呢,例如O'Reilly。

这样就剩下参数和存储过程了。

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

评论

0赞 Rune Grimstad 5/26/2009
双撇号将由 sql server 转换为单撇号,因此 O'Reilly 将被转换为 Name = 'O''Reilly'
0赞 quamrana 5/26/2009
那么,当用户想要查看他们的数据时,是否有相应的功能可以删除撇号呢?
0赞 cHao 12/12/2013
没必要。转义序列允许分析器查看单个引号,而不是字符串的末尾。在解析时,它被视为文字,因此您的字符串将在内部被视为字符序列。这就是数据库将存储、检索、比较等内容。如果要在转义后向用户显示其数据,请保留未转义字符串的副本 appside。'''O'Reilly
3赞 joshcomley 5/26/2009 #8

只有当你保证要传入一个字符串时,这才是安全的。

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

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

最终将变成:

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

评论

0赞 Andomar 5/26/2009
它要么是字符串,要么是数字。字符串使用 SafeDbString 进行转义。数字是 Int32,它不能删除数据库。
0赞 Rune Grimstad 5/26/2009
数字更容易处理。您只需将参数转换为 int/float/anything,然后在查询中使用它。问题在于何时必须接受字符串数据。
0赞 joshcomley 5/26/2009
Andomar - 如果你只是手动构造一个 SQL 语句,那么它的预期“类型”并不重要,你可以非常非常容易地用数字进行 SQL 注入。Rune - 我认为这过于依赖单个开发人员来记住手动解决 SQL 注入的所有细微差别。如果你只是说“使用参数”,那就很简单了,它们不会出错。
0赞 Joel Coehoorn 5/26/2009
@Andomar:NULL 呢?还是看起来像数字的字符串?
1赞 HLGEM 5/26/2009 #9

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

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

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

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

2赞 Darren Greaves 5/26/2009 #10

在安全问题上达成了一致。
使用参数的另一个原因是为了提高效率。

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

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

83赞 JulianR 5/26/2009 #11

我认为正确答案是:

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

使用参数。

评论

0赞 PSU 11/29/2019
回复“使用任何受信任的行业标准库”——你能为 .NET 推荐一个吗?根据数据库的不同,可能不止一个:SQLServer、MySQL、PostgreSQL?我一直在寻找 SQL-sanitizer,但运气不佳,所以 hsve 被迫尽我所能实现我自己的(这无疑远非万无一失)。
0赞 Powerlord 5/26/2009 #12

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

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

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

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

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

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

0赞 Dennis C 5/26/2009 #14

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

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

1赞 JasonRShaver 5/26/2009 #15

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

如果无法说服他们使用参数,还有 T-SQL 函数可能会有所帮助。它抓住了很多(所有?)逃脱的 qoute 问题。QUOTENAME

1赞 David 5/26/2009 #16

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

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

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

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

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

10赞 Jim T 5/26/2009 #17

所以我会说:

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();
    }

无论对错,它都能为我们提供所需的信息,而不会出现安全问题。

评论

1赞 John Saunders 5/27/2009
他可能不得不和一群老VBSCRIPT程序员打交道,他们习惯于通过字符串连接来做所有事情,包括XML和SQL。这些人会因为使用 API 而感到害怕。对他们无能为力,至少没有人道。
1赞 Joel Coehoorn 5/27/2009
+1 表示项目 #2,但也无法强制执行实际参数。
0赞 Paul Fisher 5/29/2009 #18

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

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

2赞 bbsimonbb 10/17/2014 #19

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

下面的类包装通常用于生成 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();
        }
    }
}
1赞 VulstaR 10/23/2014 #20

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

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

1赞 bbsimonbb 9/28/2016 #21

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

法律免责声明:我写了 QueryFirst