避免不带参数的SQL注入


109

在这里,我们正在讨论在代码中使用参数化的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函数有两个原因。首先,这是自石器时代以来一直采用的方法,其次,由于您看到了在数据库上运行的exact查询,因此调试sql语句更容易。

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

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

编辑: 总结到目前为止的答复:

  • 还没有人找到在SQL Server 2005或2008上解决SafeDBString的方法。那很好,我想呢?
  • 一些答复指出,使用参数化查询可以提高性能。原因是查询计划可以重复使用。
  • 我们还同意,使用参数化查询可以提供更易于维护的可读性更高的代码
  • 此外,始终使用参数比使用各种版本的SafeDBString,字符串到数字的转换以及字符串到日期的转换要容易得多。
  • 使用参数可以实现自动类型转换,这在我们使用日期或十进制数字时特别有用。
  • 最后:不要像JulianR所写的那样尝试自己进行安全保护。数据库供应商在安全性上花费了大量时间和金钱。我们没有办法做得更好,也没有理由应该尝试做他们的工作。

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


16
您的同事是不合时宜的。挑战他们找到一部单据来支持他们的立场。前新石器时代的观点是荒谬的,事情会改变,只有陷入石器时代的人才能适应。
annakata

1
好吧,至少您的同事可以防范各种形式的黑客攻击...他们确定所有参数化查询都可以吗?(我不是...)
Arjan Einbu,2009年

1
任何一个漏洞都不会说服他们。如果您带来了多个漏洞(这是您要的)和其他问题,并一一指出了参数将解决该问题,并且您的团队将不得不编写大量的代码来提供部分功能,则可能赢得他们。祝好运。
罗伯特·高兰德

3
即使没有单引号,您仍然可以使用逻辑来破坏代码。尝试使用用户名“ test OR 1 = 1”-您将获得所有返回的行,而不仅仅是具有用户名test的行!

1
叹。我真的不明白我们作为一个行业如何设法容忍这种不专业的行为。
jeroenh

Answers:


83

我认为正确的答案是:

不要试图自己做安全措施。使用任何可信赖的,行业标准的库,这些库都可用于您要执行的操作,而不是您自己尝试执行。无论您对安全性做出什么假设,都可能是错误的。尽管您自己的方法看起来安全(最好看起来好像很摇晃),但是您有可能忽略某些东西,而在安全性方面您真的想借此机会吗?

使用参数。


关于“使用现有的任何受信任的行业标准库”-您可以为.NET推荐一个吗?取决于数据库,也许不止一个:SQLServer,MySQL,PostgreSQL?我一直在寻找SQL-sanitizer,但运气不佳,因此hsve被迫尽我所能实现自己的实现(这无疑是万无一失的)。
PSU

72

然后有人去使用“代替”。IMO是唯一安全的方法。

它还避免了很多带有日期/数字的i18n问题;03年1月2日是什么日期?123,456多少钱?您的服务器(应用程序服务器和数据库服务器)彼此同意吗?

如果风险因素不能令人信服,那么性能如何?如果使用参数,则RDBMS可以重新使用查询计划,从而提高性能。它不能仅使用字符串来执行此操作。


我已经尝试过格式化和性能参数,但是他们仍然没有被说服。
符文格里姆斯塔德2009年

5
实际上,无论是否使用参数,sql server都可以重用查询计划。我同意其他参数,但是在大多数情况下,参数化sql的性能参数不再有效。
tnyfst

1
@tnyfst:当查询字符串针对每个参数值组合更改时,它可以重用执行计划吗?我认为不可能。
约翰·桑德斯

4
如果查询文本与先前的查询文本相同,则将重新使用查询计划。因此,如果两次发送EXACT SAME查询,它将被重用。但是,如果仅更改空格,逗号或其他内容,则必须确定新的查询计划。
marc_s

1
@马克:我不确定你是完全正确的。SQL Server缓存特性有点怪异。解析器能够识别文本中的常量,并且可以人为地将SQL字符串转换为use参数之一。然后可以将这个新的参数化查询的文本插入缓存。随后的类似SQL可能会在缓存中找到与之匹配的参数化版本。但是,参数化版本并不总是与原始SQL版本一起使用,而是要缓存,我怀疑SQL有大量与性能相关的原因,可以在两种方法之间进行选择。
AnthonyWJones

27

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

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

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

我的猜测是,实际的问题是骄傲和固执,对此您无能为力。


19

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

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

这是参数为您完成的另一件事:您不必担心值是否需要用引号引起来。当然,您可以将其内置到函数中,但是然后您需要为函数增加很多复杂性:如何知道“ NULL”(作为null)和“ NULL”(仅作为字符串)或数字与一个恰好包含很多数字的字符串。它只是bug的另一个来源。

另一件事是性能:参数化查询计划的缓存通常比连接计划的缓存要好,因此在运行查询时可能使服务器节省了一步。

此外,转义单引号还不够。 许多数据库产品都允许使用其他方法来转义攻击者可以利用的字符。例如,在MySQL中,您还可以使用反斜杠对单引号进行转义。因此,以下“ name”值将使SafeDBString()函数仅使MySQL崩溃,因为当您将单引号加倍时,第一个仍被反斜杠转义,而第二个仍为“ active”:

x \'或1 = 1;-


此外,JulianR还提出了以下要点:切勿尝试自行 进行安全性工作。即使进行了全面的测试,也可以通过似乎有效的细微方式使安全编程出错,这很容易。然后时间流逝,一年后的六个月前,您发现系统被破解,直到那时您才知道。

始终尽可能依赖于为平台提供的安全性库。它们将由从事安全性代码工作的人员编写,经过比您可以管理的功能更好的测试,并且如果发现漏洞,则由供应商提供服务。


5
替换功能添加了撇号
Rune Grimstad

5
然后,它只是错误的另一个来源。如何知道NULL(作为空值)和NULL(作为文本字符串)之间的区别?还是在数字输入和恰好包含数字的字符串之间?
Joel Coehoorn

好点子。您仅应将函数用于字符串,甚至可能用于日期,因此必须小心。这是使用参数的另一个原因!好极了!
符文格里姆斯塔德2009年

10

所以我会说:

1)为什么要尝试重新实现内置的功能?它在那里,随时可用,易于使用,并且已经在全球范围内调试。如果发现将来的错误,它们将被修复,并且很快就可以提供给所有人,而您无需执行任何操作。

2)有哪些流程可以确保不会错过对SafeDBString的调用?仅在1个地方丢失它可能会带来很多问题。您打算花多少钱来考虑这些事情,并考虑在容易获得可接受的正确答案时浪费了多少精力。

3)您确定已经掩盖了Microsoft(数据库和访问库的作者)在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();
    }

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


1
他可能需要与一群老的VBSCRIPT程序员打交道,他们习惯于通过字符串串联来完成包括XML和SQL在内的所有工作。这些人会因使用API​​而感到害怕。他们没有什么可以做的,至少没有人道的。
约翰·桑德斯

1
项目2的+1,但也没有办法强制使用实参数。
乔尔·科洪

7

使用参数化查询,您可以获得更多的防止SQL注入的保护。您还将获得更好的执行计划缓存潜力。如果使用sql服务器查询探查器,您仍然可以看到“在数据库上运行的确切sql”,因此就调试sql语句而言,您实际上也不会损失任何东西。


MySQL还记录参数化查询,并在其中插入参数值。
比尔·卡温

5

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


2
+1是因为mysql_real_escape_string()会转义\ x00,\ x1a,\ n \ r'和“。它还处理字符集问题
。OP

4

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

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

当参数化查询非常易于使用时,我总是对防止SQL注入的“劳累式”方法持怀疑态度。第二,仅仅因为“总是那样做”并不意味着它是正确的方法。


3

仅在确保您要传递字符串时,这才是安全的。

如果您在某个时候不传递字符串怎么办?如果您仅传递数字怎么办?

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

最终将成为:

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

它可以是字符串或数字。使用SafeDbString对字符串进行转义。数字是Int32,它不能删除数据库。
安多玛尔

数字更易于处理。您只需在查询中使用参数之前将其转换为int / float / what。问题是何时必须接受字符串数据。
符文格里姆斯塔德2009年

Andomar-如果您只是手工构造一条SQL语句,那么它的意图“类型”无关紧要,您可以非常,非常容易地用数字注入SQL。符文-我认为这过于依赖单个开发人员来记住手动解决SQL注入的所有细微差别。如果您只是说“使用参数”,这非常简单,而且不会出错。
joshcomley,

@Andomar:NULL呢?还是看起来像数字的字符串?
乔尔·科洪

2

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

在必须将SQL放入代码的地方,我使用参数,这是唯一有意义的事情。提醒异议者,他们有比自己聪明的黑客,并且更有动机破坏试图超越他们的代码。使用参数根本不可能,也不是那么困难。


好的,如何使用参数进行SQL注入?
约翰·桑德斯

@Saunders:步骤1是在数据库的参数处理功能中找到缓冲区溢出错误。
布赖恩

2
找到一个了吗?在一个每天被成千上万的黑客所打击的商业数据库中?由一家知名的软件公司生产的产品,有很多钱?如果可能的话,您可以按名称引用诉讼。
约翰·桑德斯

1
当然,如果SPROC使用串联和EXEC(而不是sp_ExecuteSQL),您将再度遇到麻烦...(我见过它做错了太多次,无法打折...)
Marc Gravell

2

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

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

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


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

1

从我必须调查SQL注入问题的很短时间内,我可以看到,将值设置为“安全”也意味着您关闭了可能实际上想要在数据中使用撇号的情况-某人的名字呢,例如O'Reilly。

剩下参数和存储过程。

是的,您应该始终以目前已知的最佳方式来尝试实现代码,而不仅仅是始终完成代码。


双引号会由SQL Server转换成单引号,所以奥赖利将被翻译成名称=“O''Reilly”
符文格里姆斯塔

那么,当用户希望查看其数据时,是否有相应的功能来删除撇号?
quamrana

没必要。转义序列使解析器可以看到单引号而不是字符串的结尾。解析时,它看起来''像是文字',因此您的字符串将在内部被视为字符序列O'Reilly。这就是数据库将要存储,检索,比较的内容,等等。如果要在转义后向用户显示其数据,请保留未转义的字符串appside的副本。
cHao 2013年

1

以下几篇文章可能对说服同事很有帮助。

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

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

我个人不希望任何动态代码都不能接触我的数据库,要求所有联系人都通过sps(而不是使用动态SQl的联系人)进行。这意味着我无法授予用户任何权限,内部用户(除了极少数具有管理访问权限的生产用户)无法直接访问我的表并造成破坏,窃取数据或进行欺诈。如果您运行财务应用程序,这是最安全的方法。


1

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

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

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

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

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


1

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

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


1

我没有看到其他答案回答“为什么这样做很不好”的这一方面,但是考虑使用SQL Truncation攻击

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


1

2年后,我辞职了...欢迎任何发现参数麻烦的人尝试我的VS扩展QueryFirst。您可以在真实的.sql文件(验证,Intellisense)中编辑请求。要添加参数,只需将其直接输入到SQL中即可,并以“ @”开头。保存文件时,QueryFirst将生成包装器类,以使您可以运行查询并访问结果。它将查找参数的DB类型并将其映射为.net类型,您可以将其作为生成的Execute()方法的输入。再简单不过了。正确地执行此方法比执行其他任何方法都更快,更容易,并且创建sql注入漏洞变得不可能,或者至少是有害的。还有其他杀手级优势,例如能够删除数据库中的列并立即在应用程序中看到编译错误。

法律免责声明:我写了QueryFirst


0

以下是使用参数化查询的一些原因:

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

0

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

我想说的是,SQL注入不只是“转义引用”,而且您不知道接下来会发生什么。


0

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.