当参数不是字符串时,不对SQL查询进行参数化是否安全?


113

SQL注入方面,我完全理解了参数化string参数的必要性。那是书中最古老的把戏之一。但是什么时候可以证明不对参数进行参数化SqlCommand呢?是否将任何数据类型视为“安全的”而不进行参数化?

例如:我不会认为自己是SQL专家附近的任何地方,但是我无法想到在任何情况下都可能容易接受SQL注入以接受a bool或an int并将其直接连接到查询中。

我的假设是正确的,还是可能在程序中留下巨大的安全漏洞?

为了澄清起见,标记了这个问题 这是一种强类型的语言;当我说“参数”时,请想一想 public int Query(int id)


14
如果不使用参数,则不会获得缓存查询计划的好处,它需要为您提供的每种新输入组合制定单独的查询计划。
Scott Chamberlain

5
@MatthewWhited您怎么会知道需要更少的时间?在当前开发人员和先前开发人员的某些项目中,这种情况到处都会发生。如果确实提高了安全性,请发布答案。为了澄清起见,我确实同意最好进行参数化。但这不是我真正的问题。
johnnyRose15年

7
参数化查询主要用于性能和优化。SQL注入预防是一个副作用。
Salman 2015年

13
我认为OP提出了一个有效的问题。他正在尝试评估修复潜在风险的成本/收益。该等式随该风险的可能性而变化。如果风险为零,我也不会这样做。他问的是有关潜力的技术问题,而不是主观判断您是否认为自己值得他花时间。OP是唯一可以拨打该电话的人。
Swears-a-Slot爵士

10
自我解释:我是dba。我赞赏并尊重最佳实践,在一个完美的世界中,所有代码都是完美的。可悲的是,在我工作的世界中,我要解决的问题比我有时间解决的问题还要多。这意味着优先级高。IMO重写已经有效,安全并且性能达到可接受水平的代码听起来像是奢侈品。(我负担不起)
瑟斯爵士乐团

Answers:


101

技术上讲,我认为这是安全的,但是要养成一个可怕的习惯。您真的要编写这样的查询吗?

var sqlCommand = new SqlCommand("SELECT * FROM People WHERE IsAlive = " + isAlive + 
" AND FirstName = @firstName");

sqlCommand.Parameters.AddWithValue("firstName", "Rob");

在类型从整数更改为字符串的情况下,这也使您容易受到攻击(请考虑雇员编号,尽管其名称可能包含字母)。

因此,我们已将EmployeeNumber的类型从更改intstring,但是忘记更新我们的sql查询。哎呀


24
我们可以停止使用AddWithValue 了吗?blogs.msmvps.com/jcoehoorn/blog/2014/05/12/…–
RemarkLima

5
@RemarkLima当您动态生成将值映射到参数的代码时,解决方案是什么?该博客文章无法解决这种情况。是的,这是一个单行设置SQL类型时,它知道,但如果它不是,那么你有一个问题(或者你不得不求助于标注型号的信息)。
casperOne 2015年

1
然后,AddWithValue除非您将数据库类型的映射作为语句动态构建的一部分,否则您将陷入困境。我假设您有一个目标列的列表,并且作为字典的一部分,如果您愿意,可以有类型。否则,只需应对性能问题。最终,了解我的想法只是很好的信息。
备注利马2015年

8
@RemarkLima关键是“我们可以停止使用AddWithValue了吗?” 要真正做到“如果你知道的类型,那么你应该使用避免AddWithValue
casperOne

2
嘿,别开枪了,我没有写信;-)但是重点仍然存在,如果从一开始就将其内置到您的设计中,就没有理由不知道类型了。最佳实践和所有爵士乐:-)
RemarkLima

65

当你使用控制(如Web服务器)的计算机上的强类型的平台,可以防止代码注入的查询只boolDateTimeint(及其他数字)值。令人担忧的是性能问题,这是由于强制sql Server重新编译每个查询,以及阻止它获取有关以何种频率运行哪些查询的良好统计信息而造成的(这不利于缓存管理)。

但是,“在您控制的计算机上”这一部分很重要,因为否则用户可以更改系统用于从这些值生成字符串的行为,以包括任意文本。

我也想长远考虑。当今天的旧式强类型代码库通过自动翻译移植到新的动态语言中,而您突然失去类型检查,但还没有针对动态代码进行所有正确的单元测试时,会发生什么情况?

确实,没有充分的理由不对这些值使用查询参数。这是正确的方法。继续,当它们确实是常量时,将值硬编码到sql字符串中,但是否则,为什么不只使用参数呢?这并不难。

最终,我本质上不会称它为bug,但我会称其为气味:某些东西本身就没有bug,但却有力地表明bug在附近或最终会出现。好的代码可以避免产生气味,任何好的静态分析工具都会对此进行标记。

我要补充一点,不幸的是,这不是您可以直接赢得争论的那种。听起来像是“权利”不再足够了,踩着你的同事自己解决这个问题的可能性不大可能促进良好的团队动力。它最终可能带来的伤害超过其帮助。在这种情况下,更好的方法可能是促进使用静态分析工具。这将使旨在并回溯并修复现有代码的工作具有合法性和可信性。


1
对其进行参数化绝对不是挑战。之所以引起我的问​​题,是因为一位同事写了一堆连接整数值的查询,而我想知道是否浪费我的时间来解决所有这些问题。
johnnyRose 2015年

2
我认为“是虫子”是我的问题。
johnnyRose2015年

7
这是一种“气味”:本身并没有缺陷,但是表明缺陷可能在附近。好的代码试图消除气味。任何好的静态分析工具都将对其进行标记。
Joel Coehoorn

1
我喜欢“气味”一词。我本来会用“幼虫”之类的东西代替,虽然它还不是一个小虫,但是将来的更新可能会将其捕获到一个got虫中,直到您将其粉碎或熏蒸之前,它都会在您的后端吃掉。您当然不希望在生产环境中出现坏死代码的可能性,在这种情况下,拥有某些未经过一定技巧开发的东西肯定会导致这种情况的出现。
CSS

1
这是错误的。例如,请参见我的答案,以了解如何仍可以使用DateTimeint
Kaspars Ozols 2015年

53

在某些情况下,可以使用字符串值以外的非参数化(串联)变量执行SQL注入攻击-请参阅乔恩的这篇文章:http : //codeblog.jonskeet.uk/2014/08/08/the-bobbytables -文化/

问题是,当ToString被调用时,某些自定义区域性提供程序可以将非字符串参数转换为其字符串表示形式,从而将一些SQL注入查询中。


14
我认为这是回答问题的唯一帖子,本质上是“使用ints怎么可能进行注入?”
Arturo TorresSánchez15年

3
尽管如果您能够注入自定义代码(例如booby trapped),则CultureInfo很难知道为什么仍然需要进行SQL注入。
马丁·史密斯

加1,唯一可以真正回答问题的答案
ken2k

@MartinSmith:请参阅我的回答,其中显示了一种从外部更改CultureInfo的可能方法。
user1027167

如果一个人可以在应用程序中编写这样的代码,为什么他需要SQL注入?
Reza Aghaei 2015年

51

即使对于非字符串类型,这也不安全。始终使用参数。期。

考虑下面的代码示例:

var utcNow = DateTime.UtcNow;
var sqlCommand = new SqlCommand("SELECT * FROM People WHERE created_on <= '" + utcNow + "'");

乍一看,代码看起来很安全,但是如果您在Windows区域设置中进行了一些更改并以短日期格式添加了注入,那么一切都会发生变化:

日期时间注入

现在生成的命令文本如下所示:

SELECT * FROM People WHERE created_on <= '26.09.2015' OR '1'<>' 21:21:43'

int用户可以对类型进行相同的操作,因为用户可以定义自定义负号,可以轻松地将其更改为SQL注入。

有人可能会争辩说应该使用不变区域性而不是当前区域性,但是我已经多次看到这样的字符串连接,并且使用时将字符串与对象连接起来很容易错过+


10
谁可以更改服务器设置?如果某人可以在您的服务器上完成此操作,则无需SQL注入即可销毁数据。
Reza Aghaei 2015年

5
这是最好的答案,它显示了一种验证操作员是否存在漏洞/安全缺陷的方法。除了在SQL中连接日期时间之外,性能和将来的证明还不仅仅只是一种气味技术负担。@RezaAghaei这个问题从未提到服务器端,它可能是带有SQLExpress的Windows应用程序-两种方式都不是问题的标准。任何人都可以说但有权访问服务器设置的人可以反驳这个绝妙的答案,就像任何人都可以说共享服务器托管或Y2K错误一样。我确实同意您所锁定的服务器-但这不是先决条件。
杰里米·汤普森

2
您能否提供一个有关int类型的示例?
johnnyRose15 2015年

1
我知道距您回答这个问题已经有几个星期了,但是您可以编辑帖子并添加一个示例来定义自定义负号的例子吗?
johnnyRose 2015年

23

“ SELECT * FROM Table1 WHERE Id =” + intVariable.ToString()


安全
可以。
攻击者无法在您键入的int变量中注入任何内容。

性能
不佳。

最好使用参数,因此查询将被编译一次并缓存以备下次使用。下次即使使用不同的参数值,查询也会被缓存,并且不需要在数据库服务器中进行编译。

编码风格
不好的做法。

  • 参数更易读
  • 也许它使您习惯于不带参数的查询,然后也许您曾经犯过一次错误,并以这种方式使用字符串值,那么您可能应该对数据说再见了。坏习惯!


“选择*从产品WHERE Id =” + TextBox1.Text


虽然这不是您的问题,但可能对将来的读者有用:

安全
灾难!
即使该Id字段为整数,您的查询也可能需要进行SQL注入。假设您的应用程序中有一个查询"SELECT * FROM Table1 WHERE Id=" + TextBox1.Text,攻击者可以在文本框中插入1; DELETE Table1查询,该查询将为:

"SELECT * FROM Table1 WHERE Id=1; DELETE Table1"

如果您不想在此处使用参数化查询,则应使用键入的值:

string.Format("SELECT * FROM Table1 WHERE Id={0}", int.Parse(TextBox1.Text))


你的问题


之所以引起我的问​​题,是因为一位同事写了一堆连接整数值的查询,我想知道这是否浪费了我的时间来修复所有这些值。

我认为更改这些代码并不浪费时间。确实建议更改!

如果您的同事使用int变量,则没有安全风险,但是我认为更改这些代码并不浪费时间,因此建议您更改这些代码。它使代码更易读,更可维护,并使执行速度更快。


就安全而言,即使第一种选择也不是完全可以的。的行为.ToString()由易于更改为包含任意文本的操作系统配置项确定。
Joel Coehoorn

18

实际上有两个问题合而为一。标题中的问题与OP在随后的评论中表达的关注关系不大。

尽管我意识到对于OP来说,最重要的是他们的特殊情况,但对于来自Google的读者来说,回答更笼统的问题很重要,可以将其表述为:“如果我确定,串联与准备好的语句一样安全吗?我连接的每个文字都是安全的吗?”。因此,我想专注于后者。答案是

绝对没有

解释并不像大多数读者所希望的那样直接,但是我会尽力而为。

我已经考虑了一段时间,导致这篇文章(尽管基于PHP环境)试图对所有内容进行总结。在我看来,防止SQL注入的问题通常不涉及某些相关但较狭窄的主题,例如字符串转义,类型转换等。尽管某些措施单独采取可以被认为是安全的,但是没有系统,也没有简单的规则可循。这使得它非常湿滑,从而过多地吸引了开发人员的注意力和经验。

SQL注入问题不能简化为某些特定语法问题。它比一般开发人员以前想的要宽。这也是一个方法论上的问题。它不仅是“我们必须应用哪种特定格式”,还包括“必须如何完成”。

(从这个角度来看,在另一个答案中引用的Jon Skeet的一篇文章做的不好而不是好,因为它再次挑剔一些极端的情况,专注于特定的语法问题,而未能整体解决该问题。)

当您尝试不整体解决保护问题,而是解决一系列不同的语法问题时,您将面临众多问题。

  • 可能的格式选择列表非常庞大。意味着可以轻易地忽略一些。或混淆它们(例如,通过使用字符串转义标识符)。
  • 串联意味着所有保护措施都必须由程序员而非程序来完成。仅此问题会导致多种后果:
    • 这样的格式是手动的。手动意味着极易出错。可能会忘记申请。
    • 此外,还存在将格式设置过程移至某些集中功能,甚至使事情变得更加混乱以及破坏不准备进入数据库的数据的诱惑。
  • 当涉及到多个开发人员时,问题将成倍增加十倍。
  • 使用串联时,无法一目了然地告诉一个潜在的危险查询:它们具有潜在的危险!

与那堆混乱不同,准备好的声明确实是圣杯:

  • 它可以以一种易于遵循的简单规则的形式表示。
  • 它本质上是不可分离的措施,意味着开发人员不能干预,并且愿意或不愿意破坏过程。
  • 防止注入实际上只是准备好的语句的副作用,其真正目的是产生语法正确的语句。句法正确的陈述是100%注入证明。尽管有注入的可能性,但我们仍需要语法正确。
  • 如果一直使用,则无论开发人员的经验如何,它都能保护应用程序。说,有一种叫做二阶注入的东西。还有一个非常强烈的错觉,上面写着“为了保护, 转义所有用户提供的输入 ”。如果开发人员可以自由决定哪些内容需要保护,哪些不需要,则将它们组合在一起就可以注入。

(进一步思考,我发现当前的占位符集不足以满足现实生活的需求,必须扩展它们,这对于复杂的数据结构(例如数组,甚至是SQL关键字或标识符),有时都必须添加到其中。也可以动态查询,但是开发人员在这种情况下无需武装,而是被迫退回到字符串连接,但这是另一个问题。

有趣的是,这个问题的争议是由Stack Overflow有争议的性质引起的。该网站的想法是利用直接问用户特定问题,以实现拥有适用于来自搜索用户的通用答案数据库的目标。这个想法本身并不坏,但是在如下情况下会失败:当用户提出一个非常狭窄的问题时,尤其是在与同事的争执中引起争论(或确定是否值得重构代码)。当大多数有经验的参与者试图写答案时,请记住任务 全面了解Stack Overflow,使他们的回答对尽可能多的读者(而不只是OP)有好处。


10
未回答问题
edc65

大多数数据库通过SQL字符串相等性来检测已经使用过的参数化查询,因此旧的prepare-and-use-handle方法对我而言似乎已过时。这些句柄只能在一定范围内使用,并且需要进行编码以跟踪该句柄。我认为应直接使用参数化查询,以便可以在没有句柄跟踪的情况下甚至在不同的应用程序之间重复使用查询计划。
Erik Hart 2015年

15

我们不仅要考虑安全性或类型安全方面的考虑。

使用参数化查询的原因是为了提高数据库级别的性能。从数据库的角度来看,参数化查询是SQL缓冲区中的一个查询(使用Oracle的术语,尽管我认为所有数据库内部都有类似的概念)。因此,数据库可以在内存中保存一定数量的查询,这些查询已经准备就绪并且可以执行。这些查询不需要解析,并且会更快。经常运行的查询通常位于缓冲区中,并且每次使用时都不需要解析。

除非

有人不使用参数化查询。在这种情况下,缓冲区将由几乎相同的查询流连续刷新,每个查询都需要由数据库引擎进行解析和运行,并且由于频繁运行的查询最终会多次被重新解析,因此性能会受到全面影响。天。我已经调整了数据库为生,这一直是低挂水果的最大来源之一。

现在

要回答您的问题,如果查询中包含少量不同的数字值,则可能不会引起问题,并且实际上可能无限地提高性能。但是,如果可能有数百个值,并且查询被调用很多,那么您将影响系统的性能,因此请不要这样做。

是的,您可以增加SQL缓冲区,但最终最终总是以其他更关键的内存使用为代价,例如缓存索引或数据。道德,请谨慎地使用参数化查询,以便您可以优化数据库并为重要内容使用更多服务器内存...


8

要将一些信息添加到Maciek,请执行以下操作:

通过反射调用程序集的主要功能,很容易更改.NET第三方应用程序的区域性信息:

using System;
using System.Globalization;
using System.Reflection;
using System.Threading;

namespace ConsoleApplication2
{
  class Program
  {
    static void Main(string[] args)
    {
      Assembly asm = Assembly.LoadFile(@"C:\BobbysApp.exe");
      MethodInfo mi = asm.GetType("Test").GetMethod("Main");
      mi.Invoke(null, null);
      Console.ReadLine();
    }

    static Program()
    {
      InstallBobbyTablesCulture();
    }

    static void InstallBobbyTablesCulture()
    {
      CultureInfo bobby = (CultureInfo)CultureInfo.InvariantCulture.Clone();
      bobby.DateTimeFormat.ShortDatePattern = @"yyyy-MM-dd'' OR ' '=''";
      bobby.DateTimeFormat.LongTimePattern = "";
      bobby.NumberFormat.NegativeSign = "1 OR 1=1 OR 1=";
      Thread.CurrentThread.CurrentCulture = bobby;
    }
  }
}

仅当BobbysApp的主要功能是公开的时,此方法才有效。如果Main不是公共的,则可能会调用其他公共函数。


1
您甚至不必通过代码进行操作。您可以直接在Windows区域设置中添加注入。看我的答案。
Kaspars Ozols

2
谁可以更改服务器设置或谁可以在服务器中摩擦此类代码?如果某人可以在您的服务器上完成此操作,则无需SQL注入即可销毁数据。
Reza Aghaei 2015年

7

我认为,如果可以保证使用的参数永远不会包含字符串,那么它是安全的,但无论如何我都不会这样做。另外,由于您正在执行串联,因此您会发现性能略有下降。我要问的问题是,为什么不想使用参数?


1
不是我不想使用参数,而是使用参数。一位同事写了这样的代码,今天我修改为参数化代码,这让我想到了这个问题。
johnnyRose15年

好。大。使用参数是最佳做法。这样,您不必担心sql注入之类的事情。同样,如果您要构建动态查询,则无论查询多么复杂,也可以使用参数。在构建它们时只需使用@ 1 ... @ n样式。并将它们附加到具有所需值的参数集合中。
最大

@johnyRose使用参数还有一点:程序在不断发展和变化。您只能将串联用于字符串,但不能保证有人实施重构会更改某些参数类型,并且所做的更改会引入SQL注入漏洞。
lerthe61

3

没关系,但是永远都不安全..并且安全性始终取决于输入,例如,如果输入对象是TextBox,则攻击者可能会做一些棘手的事情,因为textbox可以接受字符串,因此您必须进行某种验证/转换才能防止用户输入错误。但事实是,这并不安全。就这么简单。


那是一个字符串。我说的是其他数据类型,例如整数,布尔值或日期时间。
johnnyRose 2015年

@johnnyRose是的,我在上面看到了一个非常不错的示例,您将其标记为由Kaspards回答。并且很好的答案是,他使用datetime数据类型作为示例,这种情况并不常见。:)我希望您已经确信在任何类型的数据类型中使用参数都是不安全且更好的方法
japzdivino 2015年

我从来没有真正怀疑过使用参数更安全。我的问题涉及强类型的实现。
johnnyRose 2015年

是的。我同意。这也是一个可以帮助未来读者的好问题:)
japzdivino 2015年

-2

不,您可以通过这种方式获得SQL注入攻击。我用土耳其语写过一篇老文章,说明了这里的用法。PHP和MySQL中的文章示例,但在C#和SQL Server中概念相同。

基本上,您会按照以下方式进行攻击。假设您有一个页面,该页面根据整数id值显示信息。您不会像下面这样参数化此值。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=24

好的,我假设您正在使用MySQL,并且按照以下方式进行攻击。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=ASCII((SELECT%20DATABASE()))

请注意,这里注入的值不是字符串。我们正在使用ASCII函数将char值更改为int。您可以使用“ CAST(YourVarcharCol AS INT)”在SQL Server中完成相同的操作。

之后,我使用length和substring函数来查找您的数据库名称。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=LEN((SELECT%20DATABASE()))

http://localhost/sqlEnjeksiyon//instructors.aspx?id=ASCII(SUBSTR(SELECT%20DATABASE(),1,1))

然后使用数据库名称开始在数据库中获取表名称。

http://localhost/sqlEnjeksiyon//instructors.aspx?id=ASCII(SUBSTR((SELECT table_name FROM INFORMATION_SCHEMA.TABLES LIMIT 1),1,1))

当然,您必须自动执行此过程,因为每个查询只能得到一个字符。但是您可以轻松地自动化它。我的文章在watir中显示了一个例子。仅使用一页,而不使用参数化的ID值。我可以了解您数据库中每个表的名称。之后,我可以寻找重要的表格。这需要时间,但可行。


2
我的问题涉及强类型语言。尽管您的解释对于灵活输入的语言非常有用,但是插入的值仍然是字符串。
johnnyRose

没有注入值是整数。您得到char并使用ASCII MySQL函数将其更改为整数。您在SQL Server中使用CAST(YourCharValue AS INT)做同样的事情
Atilla Ozgur

我的意思是这样的:public void QuerySomething(int id) { // this will only accept an integer }
johnnyRose

-3

好吧……可以肯定的是:当您将一个字符串(由用户接受)与您的SQL命令字符串连接在一起时,安全性就不行了。无论where子句引用一个Integer或任何类型都是无关紧要的。可能会发生注射。

在SQL Injection中,重要的是用于从用户那里获取值的变量的数据类型。

假设在where子句中有一个整数,并且:

  1. 用户变量是一个字符串。那么好吧,注入(使用UNION)不是很容易,但是使用'OR 1 = 1'绕过非常容易-就像攻击...

  2. 如果用户变量是整数。再一次,我们可以通过对系统崩溃甚至隐藏的缓冲区溢出(在最后的字符串上)通过不寻常的大数字测试来“测试”系统的强度...;)

可能查询的参数或存储过程的参数(甚至更好-imo)不是100%威胁安全的,但是它们是最小化它们的最低要求的度量(或者,如果您愿意,则是基本的度量)。


我认为您可能误解了这个问题。我说的是非字符串类型。
johnnyRose15 2015年

您好JonnyRose ...感谢您的来信。我理解您的问题,但我只是将回答以更一般的形式提出。对于非字符串大小写,请检查我的(2)点。;)
Andreas Venieris
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.