在运行时,从 C 在原始 SQL 中添加列#

At runtime add columns in raw SQL from C#

提问人:Dave 提问时间:11/3/2023 最后编辑:CharliefaceDave 更新时间:11/3/2023 访问量:76

问:

我有一个SQL查询,其中子句将在运行时决定。它可以是带有 和 运算符的列的任意组合。此数据库中的存储过程受到限制,因此将使用原始 SQL。WHEREORAND

从 postman 获取 ris OData 可以是以下任何一种。

eq  Equals          /Employees?$filter=Name eq 'John'
ne  Does not equal  /Employees?$filter=Name ne 'John'
gt  Greater than    /Employees?$filter=Age gt 15
lt  Less than       /Employees?$filter=Age lt 15
ge  Greater than or equal to /Employees?$filter=Age ge 15
and /Employees?$filter=Name eq 'John' and Age gt 65
or  /Employees?$filter=Age gt 65 or Age lt 11
not /Employees?$filter=not(Name eq 'John')

然后,在构造和执行查询后,将使用 a 从数据库中获取数据。SqlDataReader

public ActionResult Get(Columns columns =null)
{
    StringBuilder str = new StringBuilder();
    str.Append("select firstNmae,lastname,gender,city from dbo.customer where ");

    if (columns != null)
    {
        if (columns.firstName != null || columns.firstName != "")
        {
            str.Append(String.Format("firstname = '{0}' ", columns.Name));
        }

        if (columns.lastName != null || columns.lastName != "")
        {
            str.Append(String.Format("lastName = '{0}' ", columns.lastName));
        }
    }
}

我怎样才能将上述内容包含在此 SQL 中?

select firstName, lastname, gender, city 
from dbo.customer 
where + columns which comes at runtime 1 or more with or and conditions
C# SQL-Server ado.net SQLClient DynamicQuery

评论

2赞 Joel Coehoorn 11/3/2023
fwiw,我不会这样做,因为你所说的非常容易受到 sql 注入攻击,并且会让攻击者(甚至是偶然的良性用户)运行他们自己选择的任意 SQL,包括完全独立的 UPDATE 和 DELETE 查询。
1赞 Franz Gleichmann 11/3/2023
我如何使用实体框架查询生成器来代替?
1赞 Flydog57 11/3/2023
如果你打算做这样的事情,请确保你有一个非常具体的语言,并且你正在仔细解析它。不允许任何列名;将其限制为正确的。创建精确的语法,并且只允许与语法匹配的构造。然后找一个有黑客经验的人尽可能地冲击你的系统。SQL注入是阴险的;为恶毒的演员提供通往王国的钥匙很容易
0赞 Charlieface 11/3/2023
“存储过程在这个数据库中受到限制”,这是谁的脑电波,他们认为它提供了什么好处?
0赞 Dan Guzman 11/3/2023
@Charlieface,我们的一位应用架构师禁止使用存储过程来帮助确保应用程序是“云原生”的。这太疯狂了。

答:

2赞 Joel Coehoorn 11/3/2023 #1

在构建这样的 SQL 时,你必须非常小心地去做。很容易意外地对 SQL 注入问题敞开心扉。

为了安全地做到这一点(这是非常重要的领域之一,即使在学习/实践/概念验证/私人工作中也是如此),您必须做两件事:

  1. 预先验证列名(和运算符)与数据库中的实际内容。为此,可以使用视图运行快速(安全)SQL 语句,或者直接在应用程序中包含允许的列集。然后,自行提供这些项的文本,而不是直接从用户输入中提供文本。sys.columns
  2. 对筛选器数据使用查询参数而不是字符串连接。

这样,用户提供的文本就不会成为最终 SQL 语句的一部分。这很重要!

我在这里不会做的是谈论你如何解析条件。这不是一个代码编写服务,这里描述的语法很快就会超出你的预期,类似于一个功能齐全的SQL解析器。columns

但是一旦完成,你就需要得到这样的结果:

public ActionResult Get(Columns columns =null)
{
    StringBuilder str = new StringBuilder("select firstNmae,lastname,gender,city from dbo.customer where 1=1 ");
    var parameters = new List<SqlParameter>();
    SqlParameter p;

    // Assuming you already extracted your columns details
    //  from the input into separate variables used below
    //  (again: not a code writing service, you'll need to do this part)

    if (NameFilter.Length > 0)
    {
        str.Append(" AND Name = @Name");
        // use actual database types/lengths here
        p = new SqlParameter("@Name", SqlDbType.NVarChar, 20);
        p.Value = firstNameFilter;
        parameters.Add(p);
    }

    if (AgeFilter.Length > 0)
    {
        //simplified
        var opMap = new Dictionary<string, string>() {{"lt", "<"}, {"le","<="},{"eq","="},{"ne","<>"},{"gt",">"},{"ge",">="}};
        var op = opMap[AgeOperator];

        str.Append($" AND Age {op} @Age");
        // use actual database types/lengths here
        p = new SqlParameter("@Age", SqlDbType.Int);
        p.Value = Convert.ToInt32(AgeFilter);
        parameters.Add(p);
    }

    // ...

    using var conn = new SqlConnection("connection string here");
    using var cmd = new SqlCommand(str.ToString(), conn);
    foreach(var parameter in parameters)
    {
        cmd.Parameters.Add(parameter);
    }
    conn.Open();
    using var rdr = cmd.ExecuteReader();    

    // do something with your reader ...

}

请注意,在上面的代码中,从输入解析的文本通过对象的属性使用。SQL 命令中使用的所有其他文本都是从代码中的字符串文本中提取的。columnsSqlParameter.Value

要重点介绍其中一个示例:

if (AgeFilter.Length > 0)
{
    //simplified
    var opMap = new Dictionary<string, string>() {{"lt", "<"}, {"le","<="},{"eq","="},{"ne","<>"},{"gt",">"},{"ge",">="}};
    var op = opMap[AgeOperator];

    str.Append($" AND Age {op} @Age");
    // use actual database types/lengths here
    p = new SqlParameter("@Age", SqlDbType.Int);
    p.Value = Convert.ToInt32(AgeFilter);
    parameters.Add(p);
}

在上面的代码中,在解析输入后,我们通过条件语句确定列上有一个过滤器。columnsAgeif()

在此块中,我们不仅接受用户提供的列名。相反,我们单独提供列名称。if()

同样,我们允许用户告诉我们使用哪个运算符,但我们不接受直接提供的内容。相反,我们通过字典将其转换为已知的良好值。opMap

最后,我们确实完全接受了用户给我们的实际价值......但仅通过参数将其包含在查询中,因此它仍然与 SQL 命令语句隔离。age

然后我们自己把它们放在一起,这样我们就可以控制结果的结构。

这是生成动态查询的唯一安全方法。

评论

0赞 Dave 11/3/2023
感谢您的想法,但是在运行时,我不知道列将以哪种组合出现,因此必须编写这么多if条件。(或和)
0赞 Joel Coehoorn 11/3/2023
@Dave“必须写这么多if条件”。是的。是的,你会的。唯一的另一个安全选项是定义一个类,其中包含列、运算符、筛选器值和布尔前置条件的字段,并循环访问集合。但是,替换列名文本仍然需要您来执行,因此它仍然是对每个可能的列的一组有效检查。没有其他安全的方法。
0赞 Dave 11/7/2023
Joel Coehoorn:谢谢