【问题标题】:Is it possible to query a user-specified column name without resorting to dynamic SQL?是否可以在不使用动态 SQL 的情况下查询用户指定的列名?
【发布时间】:2011-09-16 07:16:37
【问题描述】:

我正在尝试使用以下签名实现 API:

public static List<string> SearchDatabase(
    string column,
    string value);

API 的实现需要构造一个带有WHERE 子句的SQL SELECT 查询,该子句使用column 参数中指定的列名。

查询:

string query =
    string.Format(
        "SELECT Name, @Column " +
        "FROM Db1 " +
        "INNER JOIN Db2 ON Db1.Id = Db2.Id " +
        "WHERE @Column = @Value");

SQL命令和参数:

SqlCommand selectCmd =
    new SqlCommand(
        query,
        connection);
selectCmd.CommandTimeout = SqlCommandTimeout;
selectCmd.Parameters.AddRange(
    new SqlParameter[]
{
    new SqlParameter("@Column", column),
    new SqlParameter("@Value", value)
});

我是这样执行的:

SqlDataReader sqlDataReader = selectCmd.ExecuteReader();
while (sqlDataReader.Read())
{
    // ...
}

问题是sqlDataReader 没有返回任何行,所以我没有进入上面的while 循环。

但是,如果我将上面查询的最后一行更改为:

"WHERE @Column = @Value");

"WHERE Vendor = @Value");

(即将列名硬编码为“供应商”)然后它就可以工作了。

从我所做的研究中我的理解是,不可能将列名作为参数传递,而只能传递我们正在查询的值。但是,它似乎让我在SELECT 子句中使用@Column 参数,而不是WHERE 子句。

由于 SQL 注入的问题,我不想求助于动态 SQL。有没有其他方法可以解决这个问题?

【问题讨论】:

  • 你不能做一个string query = string.Format("SELECT Name, " + column + " FROM Db1 INNER JOIN Db2 ON Db1.Id = Db2.Id WHERE "+Column+" = @Value");吗? ObWarning:SQL 注入!
  • @Ocaso - 是的,SQL 注入是我想要避免的。

标签: c# sql sql-server-2005


【解决方案1】:

不幸的是表名,列名不能参数化。因为您知道表的结构,所以您可以在此参数中包含可能的列名的白名单,然后对其使用字符串连接以避免 SQL 注入:

"WHERE " + Sanitize(column) + " = @Value");

【讨论】:

  • 是的,这是一个解决方案,但并不理想。每次向我的数据库添加新列时,我可能都必须更新 Sanitize() 的实现。
  • @LeopardSkinPillBoxHat,您可以使用 ADO.NET 查询您的数据库一次(例如在应用程序启动时),以便读取表的架构并获取所有可能的列名并将其存储应用程序整个生命周期的白名单,将由 Sanitize 函数使用。
  • @LeopardSkinPillBoxHat:可能并不理想,但这是您唯一的选择。
  • 这个解决方案对我来说效果最好——先查询列并构建白名单,然后结合白名单使用动态 SQL 来防止 SQL 注入。谢谢!
【解决方案2】:

动态SQL只要正确转义列名就没有问题:

string query = string.Format(
    "SELECT Name, {0} " +
    "FROM Db1 " +
    "INNER JOIN Db2 ON Db1.Id = Db2.Id " +
    "WHERE {0} = @Value",
    Delimit(column));

在哪里...

public static string Delimit(string name) {

    if(name == null) {
        throw new ArgumentNullException("name");
    } else if(name.Length == 0) {
        throw new ArgumentException("name");
    }

    return "[" + name.Replace("]", "]]") + "]";

}

【讨论】:

    【解决方案3】:

    您可以保留参数并将 select 实现为匹配列名称的不同值的 case 语句。如果列的类型不同,您可能需要转换值。

     Select case 
             when @column = 'UserName' then username
             when @column = 'Email' then email 
             Else firstname End as Column
     From MyTable
     Where value = @vendor
    

    但是,我看不出这有什么意义,只需返回所有列并选择您感兴趣的列,使用 C# 中的结果集(如果可行的话)。

    【讨论】:

    • 谢谢 - 您能否详细说明关于归还所有列的最后一点?
    • 您的意思是select * 并增加网络和数据库服务器负载?我认为这是个坏主意。
    • 我的意思是除非您动态加入列名,否则只需执行 select db1.* 并在您的阅读器中使用 reader[columnName] 动态返回特定列。
    • @hemal 它完全取决于数据。返回单行,然后它在此处或此处的附近有几列。您还可以缓存单个调用并从后续调用中提取值。这完全取决于问题的性质。有时简单的解决方案是最好的解决方案。
    • @TheCodeKing 同意您的观点,即它取决于用例(一切总是如此)。但我不建议将其作为初始方法,并且绝对能够看到选择单个列的意义。
    【解决方案4】:

    不,您不能像这样对列名进行参数化,而不是在 SQL Server 中。

    为避免构建动态查询,您可以使用这样的 CASE 表达式:

    SELECT
      Name,
      CASE @Column
        WHEN 'Age' THEN Age
        WHEN 'Weight' THEN Weight
        …
      END
    …
    

    但是请注意,在这种情况下,各种可能的列类型应该隐式地相互兼容(双关语不好)。

    【讨论】:

    • 但这意味着如果添加了新列,我需要更新查询,对吧?
    • @LeopardSkinPillBoxHat:是的,我很害怕。除非你让你的同事帮你省事。 (开个玩笑,抱歉。)
    【解决方案5】:

    大概列名已经过验证?如果没有,您可以通过添加 select 1 from &lt;column-names-table&gt; where &lt;column-name-column&gt; = @column 来添加额外的验证。

    如果您的谓词始终为=,则无需选择@Column。填充客户端数据结构所需的额外工作是微不足道的。

    编辑:@Darin 对“2011-09-16 07:24:34Z”here 的评论不是在每次调用时验证列名,而是一次初始化更好。

    【讨论】:

    • 有时查询是WHERE &lt;column-name-column&gt; LIKE "%@value%"
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-24
    相关资源
    最近更新 更多