如何在不添加不必要的引号的情况下清理闪亮应用程序中的用户 SQL 输入?

How do I sanitize user SQL input in a shiny app without adding unnecessary quotation marks?

提问人:Ike348 提问时间:3/23/2023 最后编辑:Ike348 更新时间:3/23/2023 访问量:177

问:

我正在用 R 构建一个闪亮的应用程序,通过它,用户可以编辑 SQL 数据库的内容。也就是说,我们的应用程序包含自由文本字段,每当用户对这些字段的输入被修改时,底层数据库也是如此。当然,我们担心SQL注入,并希望采取措施帮助防范它。

目前,服务器代码如下所示。我们正在使用该包与数据库进行交互。DBI

update_query <- paste0("UPDATE ", select_var$table_name, " SET ", select_var$var_name, " = ", insert,  " WHERE ", select_id, " = '", select_value, "'")
query <- dbSendStatement(con, update_query)
dbClearResult(query)

查询的五个动态组件中有四个是从代码本身生成的,不需要清理,而是用户提供的自由格式文本。insert

为了帮助防止SQL注入,我们封装在里面。我知道简单地引用输入并不能完全防止注入,但这不是重点。因此有效,除非用户提供的输入以引号开头和/或结尾。例如:insertdbQuoteStringdbQuoteString

insert <- "'a'"
update_query <- paste0("UPDATE ", select_var$table_name, " SET ", select_var$var_name, " = ", dbQuoteString(insert),  " WHERE ", select_id, " = '", select_value, "'")
query <- dbSendStatement(con, update_query)
dbClearResult(query)

这成功地插入了转义字符串,但是当表再次加载到 R 的内存中时,相关单元格的值现在是(请注意添加的引号),而不是 .因此,当用户重新启动应用程序并需要编辑该输入时,他必须在进行所需的编辑之前删除多余的字符。有没有办法确保转义用户输入实际上不会改变内容本身?"'''a'''""'a'"

文档指出,“当再次将返回的对象传递给 as 参数时,它将原封不动地返回”。然而,如果结果被强制为字符(类似于再次将表读入 R 内存时发生的情况),则它会发生变化:dbQuoteStringdbQuoteString()x

> dbQuoteString(con, "'a'")
<SQL> '''a'''

> dbQuoteString(con, dbQuoteString(con, "'a'"))
<SQL> '''a'''

> dbQuoteString(con, as.character(dbQuoteString(con, "'a'")))
<SQL> '''''''a'''''''

如何避免这些额外的引号,同时实现一些针对 SQL 注入的保护?

编辑:我应该在原始问题中提到这一点,在收到评论和答案后,我应该注意到参数化查询也会导致相同的行为。

 update_query <- paste0("UPDATE ", select_var$table_name, " SET ", select_var$var_name, " = ", "?", " WHERE ", select_id, " = '", select_value, "'")
query <- dbSendStatement(con, update_query)
dbBind(query, list(insert))
dbClearResult(query)
SQL 注入 R-DBI

评论

1赞 markalex 3/23/2023
为什么你排除了使用 with 参数?你基本上不需要改变任何东西。将查询中的值替换为其他参数,并将其他参数传递给函数。还是我在这里遗漏了什么?dbSendStatement??params
0赞 r2evans 3/23/2023
我是“永远不要将用户数据放入查询”的坚定支持者,更喜欢一直进行参数化查询。它不会增加任何开销,使查询更易于阅读,简化服务器端查询优化缓存,并完全缓解这种过度引用问题。请参阅参数化查询(以及 XKCD 的 Exploits of a Mom)。paste
0赞 Ike348 3/23/2023
感谢您的评论,参数化查询也不起作用,所以现在我排除了它们。我已经更新了问题
0赞 EdmCoff 3/23/2023
你是如何加载结果的?在字符串上多次调用 dbQuoteString 似乎很奇怪。我希望您在检索时在任何子句中调用它一次,但我不希望您在加载它时再次尝试引用它。insertwhere
0赞 Ike348 3/23/2023
启动时,表将作为数据表读入 R(即使我没有转换为 data.table,也会发生相同的行为)。如果用户编辑值,重新启动应用程序,然后再次编辑,我会多次调用。重新启动后预填充单元格时,还会有额外的引号。也许问题在于从数据库中读取表,而不是插入到其中。dbReadTable(con, table_name) %>% as.data.table()dbQuoteString

答:

2赞 EdmCoff 3/23/2023 #1

您可能希望使用 (https://www.rdocumentation.org/packages/DBI/versions/0.5-1/topics/dbBind),而不是使用 。dbQuoteStringdbBind

像这样:

insert <- "'a'"
update_query <- paste0("UPDATE ", select_var$table_name, " SET ", select_var$var_name, " = ?",  " WHERE ", select_id, " = '", select_value, "'")
query <- dbSendStatement(con, update_query)
dbBind(query, list(insert))
dbClearResult(query)

dbBind(query, list(insert))表示将只是一个问号的参数替换为 中的值,并对其进行适当的清理。insert

或者,正如 @markalex 所指出的,您可以将参数放在 .像这样:dbSendStatement

insert <- "'a'"
update_query <- paste0("UPDATE ", select_var$table_name, " SET ", select_var$var_name, " = ?",  " WHERE ", select_id, " = '", select_value, "'")
query <- dbSendStatement(con, update_query, params = list(insert))
dbClearResult(query)

大多数(所有?)现代编程语言都会有一种像这样参数化查询的方法。

与简单地手动转义值相比,这有一些优点。如果您从未在查询中放置任何用户输入,那么您不太可能因错误地执行错误而犯错误。除了引号之外,它可能会做其他聪明的转义(你不必担心这一点)。它允许您将字符串输入和非字符串输入视为相同(您根本不需要在参数周围写引号)。您的数据库可能能够更好地优化这些查询(因为至少对于某些驱动程序,它将分别传递查询和参数,因此数据库可以避免重新计算执行计划)。

评论

2赞 markalex 3/23/2023
甚至没有必要。您可以将参数直接传递给 。dbBinddbSendStatement
0赞 EdmCoff 3/23/2023
我没有意识到这一点。谢谢。我已经调整了答案,将其作为一个选项。
0赞 r2evans 3/23/2023
...和 和 ,两者都不需要 。(而且由于实际上并没有做多个打开的结果集,所以你不能保持两个+更新查询结果集的活动并重新绑定到一个或另一个,所以我还没有找到一个例子,其中使用/对,同样。dbExecutedbGetQuerydbClearResultDBIdbSendStatementdbClearResultdbExecutedbGetQuery
0赞 Ike348 3/23/2023
谢谢,但参数化查询会导致相同的行为。我已经更新了我的问题以反映这一点。
0赞 r2evans 3/31/2023
您用于条件,但不用于设置值。修复此问题,bound-params 也可以在那里使用。我建议像这样:,然后做,尽管坦率地说,允许用户设置表名是一个巨大的负担。在提交此查询之前,您应该真正验证它是否是一小群已知表之一。?paste0("UPDATE ", select_var$table_name, " SET ", select_var$var_name, " = ? WHERE ", select_id, " = ?")dbExecute(con, update_query, params=list(select_var$var_name, select_value))