保护 dplyr 免受 SQL 注入?

问题描述 投票:0回答:1

我想使用 R-Shiny 开发一个 Web 应用程序,用于访问包含敏感数据的 SQL 数据库。 到目前为止,我已经将数据库查询编写为纯 SQL 字符串。 然而,这种方法很容易受到 SQL 注入的影响。 因此,我的目的是用

dplyr
编写数据库查询,因为查询是用 R 编写的,而不是纯 SQL 编写的。 不幸的是,我还无法在网上找到有关此主题的任何更详细的信息或估计。因此我想澄清我在这个问题上的要求。
到目前为止,我只读到了关于
dbplyr::translate_sql()
函数的声明,该函数应该保护 SQL 注入: https://dbplyr.tidyverse.org/articles/sql-translation.html#vectors
由于我的数据非常机密,dbplyr / dplyr 应该提供 100% 的 SQL 注入保护。
我很高兴收到有关此主题的更多信息和评估。

更新
正如评论中所建议的,我添加了一个可重复性的示例: (改编自:https://shiny.posit.co/r/articles/build/pool-dplyr/

library(shiny)
library(DBI)
library(pool)
library(tidyverse)
library(dbplyr)

pool <- dbPool(
  drv = RMySQL::MySQL(),
  dbname = "shinydemo",
  host = "shiny-demo.csa7qlmguqrf.us-east-1.rds.amazonaws.com",
  username = "guest",
  password = "guest")

ui <- fluidPage(
  textInput("ID", "Enter your ID:", "5"),
  tableOutput("tbl"),
  numericInput("nrows", "How many cities to show?", 10),
  plotOutput("popPlot"))

server <- function(input, output, session) {
  output$tbl <- renderTable({
    pool %>% tbl("City") %>%
      filter(ID == !!input$ID)
  })
  output$popPlot <- renderPlot({
    df <- pool %>% tbl("City") %>%
      head(as.integer(input$nrows)[1]) %>% collect()
    pop <- df$Population
    names(pop) <- df$Name
    barplot(pop)
  })
}

shinyApp(ui, server)
sql r shiny sql-injection dbplyr
1个回答
0
投票

SQL 注入的威胁取决于“如何”将用户提供的文本合并到动态 SQL 查询中。因此,如果您试图防止注入,则需要评估查询数据库时使用用户输入的每种方式。 在您发布的示例中,有两条用户输入:

    input$nrows
  1. ,输入小部件仅限于数字
  2. input$ID
  3. 这是自由文本,因此 SQL 注入的风险更大。
    
    
  4. 您可以使用以下任何方法来调查每条用户输入。总的来说,我会选择的方法是清理然后审查。

这个答案重点关注您使用 dbplyr 从 R 转换为 SQL 的情况。如果使用其他方法访问数据库(例如 DBI 包),则需要不同的方法。

选项 1) 查看生成的 SQL 查询

考虑示例代码中使用

input$ID

的位置:

pool %>%
  tbl("City") %>%
  filter(ID == !!input$ID)

为了执行此操作,dbplyr 会将 dplyr 命令从 R 转换为 SQL。我们可以使用 
show_query

命令查看此翻译。

pool %>%
  tbl("City") %>%
  filter(ID == !!input$ID) %>%
  show_query()

这可能会产生类似于以下内容的 SQL 查询:

SELECT * FROM city WHERE ID == 'my_id_value'

您不需要整个 Shiny 应用程序来测试 SQL 注入。您可以简单地改变这个查询并看看会发生什么。例如,尝试一个简单的替代查询:

attempt_inject = 'my_id_value;SELECT 123' pool %>% tbl("City") %>% filter(ID == !!attempt_inject) %>% show_query()

我怀疑这会产生类似于以下的 SQL:

SELECT * FROM city WHERE ID == 'my_id_value;SELECT 123'

这似乎是 SQL 注入的有效预防方法。但您需要根据要求进行彻底测试,以确保您有信心。

选项 2)查看源代码

dbplyr 软件包是开源的,可以在线获取。您可以查看源代码以确认翻译过程中如何处理输入。

我怀疑,除非输入是

sql

类型,否则它们将被转义为文本或 SQL 对象。

有一个 

sql()

函数可以将输入转换为

sql
类型。它的用途之一是告诉 dbplyr 不要翻译函数的内容,而是按原样使用它们。这意味着,如果您的用户可以提交 R 对象作为输入,那么这就是一个重要的漏洞领域。但是,这种情况不太可能发生,因为您的应用限制用户提交文本和数字输入。

选项 3)在将输入传递给 dbplyr 之前对其进行清理

与纯 SQL 相比,R 具有多个优势,因为您可以使用 R 来验证用户的输入,然后再将其包含在 dbplyr 查询中。

将此视为应用程序的

output$tbl

组件的替代方案:

output$tbl <- renderTable({
  current_ID = as.character(input$ID)
  acceptable_ids = pool %>%
    tbl("City") %>%
    select(ID) %>%
    distinct() %>%
    collect() %>%
    pull()
  req(current_ID %in% acceptable_ids)

  pool %>%
    tbl("City") %>%
    filter(ID == !!current_ID)
})

此方法强制将用户输入转换为类型字符,并且是在 ID 列中找到的值,然后再将用户输入传递到 dbplyr。

检查的具体选择将取决于用户输入的使用方式。我经常检查用户输入不包含特殊字符(例如

;{}[]*

或空格)。

dbplyr 包含用于分隔某些输入的命令。在适用的情况下研究并使用这些命令。

注释

    req
  • 是一个闪亮的命令,如果不满足条件,则停止组件的执行。您可以在 Shiny 上下文之外使用
    stopifnot
  • renderTable
  • 语句中获取可接受的 ID 列表并不是高效的应用程序设计。这应该计算一次并重复使用多次。
    
        
© www.soinside.com 2019 - 2024. All rights reserved.