C#参数化查询针对Oracle的严重和危险漏洞!

40

这真是一个大错特错的问题。我简直不敢相信自己的眼睛,如果这是C#中的一个真正的错误,我也不敢相信之前没有人会发现它,所以我将其放出来供其他开发者社区告诉我我做错了什么。我相信这个问题将涉及到我说“噢!”并用手掌猛击自己的头部 - 但无论如何,我们还是开始吧...

为了测试,我创建了一张表Test_1,脚本如下:

CREATE TABLE TEST_1 (
  COLUMN1 NUMBER(12) NOT NULL,
  COLUMN2 VARCHAR2(20),
  COLUMN3 NUMBER(12))
TABLESPACE USERS
STORAGE (
  INITIAL 64K
  MAXEXTENTS UNLIMITED
)
LOGGING;

现在我执行以下代码:

var conn = new OracleConnection("connectionblahblah");
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = 
  "insert into Test_1(Column1, Column2, Column3) " +
  "values(:Column1, :Column2, :Column3)";
var p = cmd.Parameters;
p.Add("Column1", 1);
p.Add("Column3", null);
p.Add("Column2", "record 1");
cmd.ExecuteNonQuery();
哇!我收到了ORA-01722错误 -“无效数字”!但是出了什么问题呢?Column1是数值型的,其值为1,所以没有问题;Column2是一个字符串,而Column3是一个可空列,所以也不应该有问题...
现在坐下来听好了...这里的问题是Column3Column2在添加到OracleParameterCollection中的顺序上被调换了。交换它们的位置,问题就解决了!
当然,这自然引导我进入下一个明显的实验...让我们像下面这样更改添加参数的代码块:
p.Add("Foo", 1);
p.Add("Bar", "record 1");
p.Add("hahahahahahaha", null);

你认为那样做会行吗?好吧,猜猜看——它确实有效!

我坐在这里,惊呆了。我简直不敢相信我所看到的,而且我也无法相信以前没有人发现这种行为(除非我不知道如何正确地使用Google)。

这不仅仅是一个烦恼——它是真正危险的。如果我错位了两个相同数据类型的列会发生什么?我甚至不会收到错误提示——我只会将错误的数据插入错误的列中,而且我自己完全不知情。

除了小心不要按照错误的顺序添加参数,有没有其他的解决方法?


5
你应该提到你正在使用Oracle提供程序(ODP.NET),而不是Microsoft提供的Oracle提供程序。前者的行为就像你所描述的那样;后者的工作方式与你期望的相同,但现在已经过时了... - Thomas Levesque
很好的问题。我也有完全相同的反应。“哇!!这是怎么回事!” - Fishcake
这是6年前的问题。现在已经是2017年了,但仍然没有改变 :( - cagri
@cagri 这是因为这是一个功能,而不是一个错误。显然。;-) - Shaul Behr
1
2022年,它依然在这里。 - Michael
显示剩余2条评论
3个回答

49

5
没错,那不是一个错误,只是一个愚蠢的“特性”。所有提供者都按名称绑定参数,但ODP.NET不会这样做,可能因为Oracle不喜欢与其他人一样... 我曾经在使用Oracle数据库时对此进行过抱怨,显然没有简单的默认方式来按名称绑定...请参见此问题以获取更多信息。 - Thomas Levesque
7
好吧,我该说什么呢?你的答案是正确的 - 但我必须同意@Thomas的看法,这是一个愚蠢的“功能”。更强烈地说:这是一个如此令人费解、误导和危险的功能,以至于应该被归类为错误。为什么会有任何人想要按顺序而不是按名称绑定他们的参数呢?而且默认情况下就有这种行为?!这绝对是令人震惊的! - Shaul Behr
2
@Shaul:我同意你的观点。但即使如此,这个错误也是Oracle的问题,而不是C#的问题。你可以在Oracle网站的ODP.Net论坛上发布这个问题,看看是否有人能给出原因。但在使用Oracle时,你会发现很多类似的小问题。 - softveda
1
我的猜测是这是一种非常愚蠢的“优化”方式。按索引绑定可以节省一些周期。为了这些周期,我花费了多个小时来弄清楚发生了什么,而且我认为我不是唯一一个这样的情况。(当然,正如已经指出的那样,这也是危险的!) - The Dag
2
细节陷阱:你期望得到某些东西(使用参数名称意味着命名参数),但当你运行它时,你会得到一个错误(如果你很幸运的话!)。然后你会被告知:“惊喜,看看细则,如果你没有阅读,那是你自己的错!”如果Oracle默认使用位置参数,他们应该只允许在SQL中使用问号,并且只能按顺序或索引分配值!如果他们让用户指定参数名称并通过名称分配值,则这是一份必须遵守的命名参数合同!我认为这个漏洞已经相当老了。 - Erik Hart
显示剩余4条评论

5

您在添加列2之前是否输错了列3?因为冒号语法表示绑定变量,对于PLSQL中的绑定变量,名称无关紧要,它们按提交顺序填充。这意味着您会尝试将列2的值设置为“记录1”,这就解释了无效数字错误的原因...

您当前的代码:

p.Add("Column1", 1);
p.Add("Column3", null);
p.Add("Column2", "record 1");

请看是否这个修改可以解决你的问题:
p.Add("Column1", 1);
p.Add("Column2", "record 1");
p.Add("Column3", null);

如何让命名参数工作?

我需要请教一位有更多C#经验的人来解释如何使命名参数起作用。但我们很高兴确认冒号似乎被解释为Oracle绑定变量。


@OMG:您并没有真正回答我的问题;您只是重申了我的发现,即参数名称被忽略了,并且它只使用它们被添加的顺序。我想通过名称添加参数,并且无需关心添加它们的顺序。我该怎么做? - Shaul Behr
@Shaul:非常抱歉。根据文档,冒号应该可以工作,但示例并未加强这一点(http://msdn.microsoft.com/en-us/library/ebxy9a8b%28VS.71%29.aspx)。看起来你应该将“:”改为“@”才能使命名参数生效。这很奇怪,因为“@”是TSQL符号。 - OMG Ponies
@Shaul:关于“公共领域中的坑”——我同意,这是一个奇怪的选择,但遗憾的是为了向后兼容性而改变它的可能性很小 :/ - OMG Ponies
1
@OMG: “酷”!!?我有其他词来形容这个,但不适合发表...!;) - Shaul Behr
3
@Shaul: 在这种情况下,我和其他人一样有幸灾乐祸的罪过。只是因为下一次可能轮到我的时候 :) - OMG Ponies
显示剩余6条评论

0
p.Add(":Column1", 1);
p.Add(":Column2", "record 1");
p.Add(":Column3", null);

//注意:我已经在参数名称前添加了“:”,以便Oracle数据客户端进行识别


这是否意味着这个漏洞终于被修复了?我在2003年就发现了相关报告。把它称为漏洞实在是低估了,我更倾向于使用类似于细节陷阱或明显的破坏的术语!如果在SQL和参数列表中都指定了参数名称,则驱动程序必须使用命名参数或引发异常,而不是将它们秘密处理为位置参数。当告诉“让它在Oracle上运行”时,没有普通开发人员会阅读这样的细节。这就像现在签署购买计算机的合同,但在细则中,您将在2年的订阅期内每月购买一台新的洗衣机! - Erik Hart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接