Back

SQL 注入初学者指南(及其防范方法)

SQL 注入初学者指南(及其防范方法)

假设你正在为应用程序构建搜索功能。用户输入产品名称,后端查询数据库,然后显示结果。看起来很简单——直到有人在搜索框中输入 '; DROP TABLE products;--

这就是 SQL 注入,它仍然是 Web 开发中最危险的漏洞之一。OWASP Top 10 将”注入”列为关键安全风险,而 SQL 注入正是该类别的核心。如果你编写的代码需要访问数据库——即使只是偶尔——你都需要了解这种攻击的工作原理以及如何阻止它。

核心要点

  • SQL 注入发生在攻击者通过用户输入插入恶意代码来操纵数据库查询时
  • 参数化查询(预编译语句)是主要防御手段,它将 SQL 代码与数据分离
  • 当使用原始查询方法时,ORM 并非自动安全
  • 对于表名和列名等动态标识符使用白名单,因为它们无法被参数化
  • 应用纵深防御:最小权限账户、集中式数据访问层以及 CI 流水线中的安全测试

什么是 SQL 注入?

SQL 注入发生在攻击者通过用户输入插入恶意代码来操纵应用程序的数据库查询时。数据库不是将输入视为数据,而是将其作为命令执行。

以登录表单为例。你的后端可能会构造这样的查询:

SELECT * FROM users WHERE email = 'user@example.com' AND password = 'secret123'

如果你通过将字符串与用户输入拼接来构建此查询,攻击者可以输入 ' OR '1'='1 作为密码。生成的查询将变成:

SELECT * FROM users WHERE email = 'user@example.com' AND password = '' OR '1'='1'

由于 '1'='1' 始终为真,查询会返回所有用户,完全绕过身份验证。

为什么 SQL 注入仍然重要

你可能认为现代框架已经解决了这个问题。实际上并没有——至少不是自动解决的。

SQL 注入漏洞出现在:

  • API 端点接受带有过滤参数的 JSON
  • 搜索表单具有多个查询选项
  • 管理后台具有动态排序或过滤功能
  • 报表生成器具有用户自定义条件

任何用户输入影响数据库查询的地方都是潜在的攻击面。后果从数据窃取到完全破坏数据库不等。

不安全的查询构造是如何发生的

根本原因始终相同:将用户输入视为可信的 SQL 代码而不是数据。

字符串拼接是主要罪魁祸首:

// 有漏洞 - 永远不要这样做
const query = `SELECT * FROM products WHERE name = '${userInput}'`;

模板字面量和字符串格式化会产生同样的问题:

# 有漏洞 - 永远不要这样做
query = f"SELECT * FROM products WHERE category = '{category}'"

即使是 ORM 也不是自动安全的,当你使用原始查询方法时:

// 有漏洞 - 原始查询绕过了 ORM 保护
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

使用参数化查询防止 SQL 注入

防御 SQL 注入的主要方法是使用参数化查询(也称为预编译语句)。这些方法将 SQL 代码与数据分离,确保用户输入永远不会作为命令执行。

以下是 JavaScript 中的安全方法:

// 安全 - 参数化查询
const query = 'SELECT * FROM products WHERE name = ?';
db.query(query, [userInput]);

Python 中的写法:

# 安全 - 参数化查询
cursor.execute("SELECT * FROM products WHERE name = %s", (user_input,))

数据库将参数视为字面值,而不是 SQL 代码。即使有人输入 '; DROP TABLE products;--,数据库也会搜索名称正好是该字符串的产品。

参数化查询无法覆盖的情况

标识符(表名、列名)无法被参数化。如果需要动态排序:

// 对列名使用白名单
const allowedColumns = ['name', 'price', 'created_at'];
const sortColumn = allowedColumns.includes(userInput) ? userInput : 'name';
const query = `SELECT * FROM products ORDER BY ${sortColumn}`;

存储过程只有在内部避免动态 SQL 时才是安全的。拼接字符串的存储过程同样容易受到攻击。

为什么转义还不够

仅靠手动字符串转义和清理是不够的。它们容易出错、依赖于特定数据库,并且很容易实现不正确。参数化查询会自动且正确地处理转义。

纵深防御

参数化查询是你的主要防御手段,但额外的防御层也有帮助:

  • 最小权限数据库账户:你的应用程序数据库用户在生产环境中不应具有管理权限,如 DROP、ALTER 或 CREATE USER
  • 集中式数据访问层:通过单一模块路由所有查询,该模块强制执行参数化
  • 代码审查和测试:在审查流程和 CI 流水线中包含 SQL 注入检查

OWASP ASVS 和 CISA 的”安全设计”倡议的现代指南强调将安全性构建到开发流程中,而不是事后添加。

结论

SQL 注入之所以仍然危险,是因为它很容易引入,而被利用时又具有毁灭性。修复方法很简单:对所有数据库操作使用参数化查询,使用白名单验证标识符,永远不要信任用户输入。

将参数化查询作为你的默认选择。你未来的自己——以及你的用户——会感谢你。

常见问题

会。NoSQL 注入是影响 MongoDB 等数据库的相关漏洞。攻击者可以通过用户输入操纵查询操作符。防御方法类似:使用数据库驱动程序的内置查询方法而不是从字符串构造查询,并在查询中使用之前验证所有用户输入。

当你使用 ORM 的标准查询方法时,它们提供保护。但是,大多数 ORM 提供绕过这些保护的原始查询功能。如果你使用原始 SQL 方法或在 ORM 查询中使用字符串插值,你仍然容易受到攻击。即使在 ORM 代码中也要始终使用参数化方法。

使用 SQLMap 或 OWASP ZAP 等自动化工具扫描漏洞。包含尝试注入攻击载荷的安全性单元测试。进行专门检查查询中字符串拼接的代码审查。在生产部署之前,考虑对关键应用程序进行渗透测试。

不够。输入验证有助于减少攻击面,但绝不应该是你唯一的防御手段。聪明的攻击者通常可以绕过验证规则。参数化查询是主要防御手段,因为它们从根本上将代码与数据分离。将验证作为额外的防御层,而不是正确查询构造的替代品。

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay