什么是 SQL 注入,如何在 C# 中防止 SQL 注入?
SQL 注入是一种代码注入技术,攻击者可通过用户输入向数据库服务器发送恶意 SQL 代码。 在"What Is SQL Injection And How Do I Prevent It in C#?"视频中,Tim Corey 准确演示了 SQL 注入漏洞如何出现在真实代码中,展示了几个成功的 SQL 注入攻击示例(包括基于联合的攻击和破坏性攻击),并介绍了可以在 C# 中应用的实用 SQL 注入防范技术。 本文紧跟 Tim 的演练,因此您可以看到他展示的确切问题和修复方法。
演示应用程序及其重要性
Tim 从一个绑定到本地 InjectableDB (人和秘密表)的小型 WPF 演示应用程序开始。 该应用程序的网络表单式搜索框接受用户输入(姓氏)并建立 SQL 查询,以返回 ID、名和姓。 它 "能工作"--输入 Corey 就能得到 Tim Corey--但 Tim 强调了核心要点:"应用程序能工作并不意味着它是安全的"。当用户提供的输入通过字符串连接或动态 SQL 直接插入 SQL 语句时,正常运行的网络应用程序仍可能存在 SQL 注入漏洞。
不安全代码--字符串连接和动态 SQL.
Tim 展示了许多开发人员使用的不安全模式:
var sql = $"SELECT * FROM People WHERE LastName = '{searchText}'";
var results = connection.Query<Person>(sql);var sql = $"SELECT * FROM People WHERE LastName = '{searchText}'";
var results = connection.Query<Person>(sql);该原始查询使用字符串连接来创建 SQL 语句。 蒂姆警告:如果您看到将用户输入直接注入 SQL 查询的代码,请停止 - 这是一个 SQL 注入漏洞。 攻击者可以制作恶意输入,更改您的 SQL 命令结构,甚至运行额外的恶意 SQL 语句。
攻击者如何利用它 - UNION 和 DROP.
为了展示 SQL 注入攻击是如何进行的,Tim 在 SQL Server 中重现了查询,然后使用 UNION ALL 和 SQL 注释 (--) 隐藏尾部字符来制作注入程序。 Tim 演示的恶意有效载荷示例:
基于联合的 SQL 注入以读取其他表:
UNION ALL SELECT ID, Username AS FirstName, Password AS LastName FROM Secrets;这将把来自 Secrets 的结果混合到原始 SELECT 结果集中,从而暴露出用户名和密码等敏感数据。
破坏性注入以删除表格:
DROP TABLE DemoTable;运行第二条 SQL 语句(DROP TABLE)时,先用分号结束第一条语句,然后添加破坏性命令。 Tim 显示表格消失了--数据库已被恶意 SQL 修改。
Tim 的观点:攻击者不需要提前知道表或列名--他们可以从数据库服务器中枚举表或列名,或者简单地尝试盲目或基于定时的技术来发现行为。
修复 1 - 参数化查询
Tim 的首要任务是停止用用户数据构建 SQL 字符串。 用参数化查询替换动态 SQL:
string sql = "SELECT * FROM People WHERE LastName = @LastName";
var results = connection.Query<Person>(sql, new { LastName = searchText });string sql = "SELECT * FROM People WHERE LastName = @LastName";
var results = connection.Query<Person>(sql, new { LastName = searchText });Tim 解释说,参数化(准备语句式使用)意味着数据库将用户提供的输入严格视为数据--恶意 SQL 只是一个字符串值,不能改变 SQL 结构。 这可以防止许多常见的 SQL 注入攻击,包括基于联合的有效载荷和附加的.NET、Java、Python 或 Node js; DROP TABLE 命令。
他还建议将参数化与最低限度的输入验证结合起来:对姓氏中不可能出现的字符(如分号或--注释标记)进行消毒或屏蔽,同时允许使用撇号等合法字符(O'Reilly)。 参数化查询 + 输入清洗可有效防止 SQL 注入攻击。
修复 2 - 存储过程
接下来,Tim 展示了两个存储过程:一个是不安全的存储过程,在存储过程中连接 SQL 然后执行;另一个是安全的存储过程,直接使用参数。
不安全的存储过程从参数构建 @sql 字符串并对其执行 - 仍易被注入。
- 安全存储过程执行 SELECT ... WHERE LastName = @LastName,并执行参数 - safe。
Tim 澄清道:如果您仍然在存储过程中构建动态 SQL,那么存储过程并不能自动治愈。 SQL语句的使用很简单,但如果使用得当(不使用动态SQL),存储过程有助于集中管理SQL语句,使参数化和审核查询变得更加容易。 存储过程还有助于简化应用程序中的 SQL 注入防范工作。
不要相信任何数据,甚至不要相信数据库数据
Tim 提出了一个经常被忽略的重要问题:不能盲目相信从自己的 SQL 数据库中获取的数据。 攻击者有时会在列中植入恶意有效载荷("定时炸弹"),这些有效载荷随后会被另一个进程连接到动态 SQL 中。 蒂姆坚持认为:无论数据来自网络表单、文件上传还是您自己的数据库,都要始终使用参数并在每个步骤中对数据进行消毒,这样恶意输入就不会成为注入的途径。
奖励提示 - 最低权限和限制数据库权限
除了代码修复外,Tim 还建议进行防御性配置:限制应用程序账户的数据库权限。 在他的演示中,连接通过集成安全功能使用了管理员账户--这很危险。 在翻译过程中,应尽量避免使用".NET"、"Java"、"Python "或 "Node js "等术语,而应使用 "最小特权 "原则:
为应用程序创建一个数据库账户,只保留其所需的权限。
如果您使用存储过程,请仅授予该账户对特定存储过程的 EXECUTE 权限,其他权限一概不授予。
- 不要赋予应用程序账户广泛的管理员权限,以允许 DROP TABLE、列出所有表或读取其他数据库。
这样可以降低 SQL 注入攻击成功后的影响--即使有可能进行注入,攻击者也无法进行超出账户允许范围的操作。
Tim 还指出,Entity Framework(Entity Framework)使翻译变得更加复杂:EF 通常需要提升权限(迁移、模式更改)。 如果您在生产中使用 EF,请仔细规划其权限和部署。
Recap - 停止、参数化、消毒、限制
在 视频的最后,Tim 提供了一份清晰的清单,用于防止 C# 应用程序中的 SQL 注入:
1.停止使用字符串连接或包含用户输入的动态 SQL 来构建 SQL 语句。
2.使用参数化查询/预处理语句模式,使用户数据始终被视为数据。
3.在适当的地方对输入进行消毒(分号块、SQL 注释、意外字符)。
4.首选安全的存储过程(内部无动态 SQL),以便集中查询逻辑。
5.对数据库账户应用最低权限--限制应用程序的数据库用户可以做什么。
6.审核代码(尤其是动态构建 SQL 的地方)并测试 SQL 注入缺陷。
蒂姆的最后警告:马虎处理用户输入、动态 SQL 和过高的数据库权限可能导致严重的漏洞--敏感数据泄露、表格毁坏或长期未被发现的外泄。 将防止 SQL 注入作为核心安全要求,而不是可有可无的修饰。

