You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何可靠验证SQL查询安全性,仅允许执行SELECT类查询并避免误判?

如何可靠验证SQL查询安全性,仅允许执行SELECT类查询并避免误判?

这个问题确实戳中了很多开发者在做SQL安全校验时的痛点——既要严格限制只能跑SELECT,又不想因为字段名、表名里的敏感词误拦合法查询,还要兼容各种复杂的SELECT语法(联表、分组、排序这些)。结合你的场景,我推荐几个从根本上解决问题的方案,从语法解析到数据库兜底,层层保障:

1. 用官方T-SQL语法解析库做精准校验(最可靠,无漏判误判)

最彻底的方法是真正解析SQL语句的语法结构,而不是靠字符串匹配敏感词。微软官方提供了Microsoft.SqlServer.TransactSql.ScriptDom NuGet包,它能把T-SQL语句解析成抽象语法树(AST),你可以遍历AST精准判断是否只有SELECT类语句,完全不会因为字段名(比如deleted_date)里的敏感词误判。

具体实现步骤:

  • 先安装NuGet包:Microsoft.SqlServer.TransactSql.ScriptDom
  • 编写语法解析和校验代码:
using Microsoft.SqlServer.TransactSql.ScriptDom;
using System.IO;
using System.Collections.Generic;

public class SqlQueryValidator
{
    public bool IsValidSelectOnlyQuery(string sql)
    {
        // 用对应版本的解析器,比如SQL Server 2019用TSql150Parser
        TSqlParser parser = new TSql150Parser(false);
        IList<ParseError> parseErrors;
        TSqlFragment sqlFragment = parser.Parse(new StringReader(sql), out parseErrors);

        // 先检查语法是否合法
        if (parseErrors.Count > 0)
        {
            return false;
        }

        // 自定义Visitor遍历AST,检查是否存在非SELECT语句
        var validationVisitor = new SelectOnlyValidationVisitor();
        sqlFragment.Accept(validationVisitor);

        return validationVisitor.IsValid;
    }

    private class SelectOnlyValidationVisitor : TSqlFragmentVisitor
    {
        public bool IsValid { get; private set; } = true;

        // 拦截所有非SELECT的DML语句
        public override void Visit(DeleteStatement node) => IsValid = false;
        public override void Visit(UpdateStatement node) => IsValid = false;
        public override void Visit(InsertStatement node) => IsValid = false;
        public override void Visit(MergeStatement node) => IsValid = false;

        // 拦截DDL语句
        public override void Visit(CreateTableStatement node) => IsValid = false;
        public override void Visit(DropTableStatement node) => IsValid = false;
        public override void Visit(AlterTableStatement node) => IsValid = false;

        // 检查所有语句节点,确保都是SELECT相关
        public override void Visit(TSqlStatement node)
        {
            // 允许的语句类型:SelectStatement 或者 仅包含SelectStatement的Batch
            if (node is BatchStatement batch)
            {
                foreach (var stmt in batch.Statements)
                {
                    if (!(stmt is SelectStatement))
                    {
                        IsValid = false;
                        break;
                    }
                }
            }
            else if (!(node is SelectStatement))
            {
                IsValid = false;
            }
        }
    }
}

这个方法的核心优势是基于语法结构判断,不管你的字段名、表名里有什么敏感词,只要是纯SELECT语句(包括联表、分组、排序、CTE等复杂语法)都会被放行,完全不会出现误判。

2. 数据库权限兜底:给执行用户仅授予SELECT权限

应用层的校验再完美,也可能存在边界情况漏判,所以数据库层面的权限控制是最后一道不可缺少的防线

你给执行查询的SQL登录账号只授予目标数据库的SELECT权限(不要给db_ownerdb_datawriter等权限),这样即使有非法语句漏过了应用层校验,数据库也会直接拒绝执行,抛出“权限不足”的错误,不会对数据造成任何修改。

示例T-SQL权限配置:

-- 创建仅具有SELECT权限的用户
CREATE LOGIN TestQueryUser WITH PASSWORD = 'StrongPassword123!';
USE YourDatabase;
CREATE USER TestQueryUser FOR LOGIN TestQueryUser;
GRANT SELECT ON SCHEMA::dbo TO TestQueryUser;
-- 如果需要访问其他 schema,再单独授予SELECT权限

3. 备选方案:用系统函数解析查询元数据

如果你不想引入第三方库,也可以用SQL Server的系统函数sys.dm_exec_describe_first_result_set来间接判断是否为SELECT语句。这个函数会解析SQL语句并返回结果集的元数据,如果是非SELECT语句(比如DELETE/UPDATE),会抛出明确的错误。

示例代码:

public bool IsSelectQueryUsingSystemFunction(string sql, SqlConnection connection)
{
    try
    {
        // 转义SQL里的单引号,避免注入
        string escapedSql = sql.Replace("'", "''");
        string checkQuery = $"SELECT * FROM sys.dm_exec_describe_first_result_set(N'{escapedSql}', NULL, 0)";
        
        using (var cmd = new SqlCommand(checkQuery, connection))
        {
            // 只要能成功执行并返回结果,说明是SELECT语句
            using (var reader = cmd.ExecuteReader()) {}
            return true;
        }
    }
    catch (SqlException ex)
    {
        // 非SELECT语句会触发特定错误,这里可以根据错误码精准判断
        // 错误码参考:11509(无法确定元数据,因为语句不是SELECT语句)
        return ex.Number != 11509;
    }
}

不过这个方法有个小缺点:对于包含变量、临时表或者动态SQL的复杂SELECT语句,可能会解析失败,不如ScriptDom的语法解析可靠,但作为轻量级的备选方案还是可以的。

关于你之前的“包装查询”思路

你提到的把查询包装成select a.* from ({Query}) a的方法,确实会因为ORDER BY失效的问题无法兼容所有场景——因为子查询里的ORDER BY只有配合TOP/OFFSET FETCH才会被数据库认可,否则会被忽略,甚至抛出错误。所以这个方法并不适合作为通用解决方案,还是语法解析的方法更靠谱。

总结

最可靠的方案是语法解析(ScriptDom)+ 数据库权限兜底

  1. Microsoft.SqlServer.TransactSql.ScriptDom做精准的语法校验,确保只有纯SELECT语句被放行;
  2. 给执行查询的SQL账号仅授予SELECT权限,作为最后一道安全防线。

这样既解决了误判问题,又能兼容所有复杂的SELECT语法,同时最大化保证了安全性。

内容来源于stack exchange

火山引擎 最新活动