尽管人们已经意识到 SQL 注入数十年,但它仍然位居 OWASP 前 10 名之列。 2023 年至 2024 年期间,医疗保健、电子商务和政府部门发生了引人注目的涉及 SQLi 的违规事件。虽然经典的 ' OR 1=1 -- 攻击众所周知,但现代变体包括二阶注入、盲目 SQLi(基于时间和基于布尔)和带外渗漏。预防措施很容易理解,但由于遗留代码、ORM 滥用和测试自动化不足而执行不力。
参数化查询和准备语句
预准备语句是预防 SQL 注入的黄金标准。它们在数据库引擎级别将 SQL 逻辑与数据分开,从而使用户输入无法更改查询结构。
// Node.js with pg — SAFE
const result = await client.query(
"SELECT * FROM users WHERE email = $1 AND status = $2",
[userEmail, "active"]
);
// Python with psycopg2 — SAFE
cursor.execute(
"INSERT INTO orders (user_id, amount) VALUES (%s, %s)",
[user_id, amount]
);
即使使用转义输入,字符串插值也永远不安全。参数化查询不会损害查询计划缓存 — 大多数数据库对参数化查询的缓存计划比临时 SQL 更有效。
| 语言 | 安全API | 不安全模式 |
|---|---|---|
| Node.js(页) | 占位符_0 | 使用 $ 进行字符串插值 |
| Python(psycopg2) | 占位符_0 | f 字符串或 % 格式 |
| Java (JDBC) | 占位符_0 | Statement 与字符串连接 |
| C#(SqlClient) | SqlCommand 与 Parameters | 带有内联 SQL 的 SqlCommand |
| PHP(PDO) | PDO::prepare() + execute() | mysqli::query() 带连接 |
诸如动态表名称或具有可变长度列表的 IN 子句之类的边缘情况需要仔细处理。对表名使用允许列表标识符,对动态列表使用 ANY 数组语法。
ORM 保护和陷阱
Prisma、TypeORM、Sequelize、Django ORM 和 Hibernate 等 ORM 通常默认会防止注入。但是,原始查询、LIKE 子句和 ORDER BY 子句会重新引入风险。
// Prisma — raw query pitfall
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE name LIKE '%${searchTerm}%'` // VULNERABLE
);
// Safe alternative with parameter binding
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE name LIKE ${"%" + searchTerm + "%"}
`;
ORDER BY 不能使用参数化列。始终根据允许列表验证列名称:
const allowedColumns = ["name", "email", "created_at"];
if (!allowedColumns.includes(sortBy)) {
throw new Error("Invalid sort column");
}
const result = await client.query(
`SELECT * FROM users ORDER BY ${sortBy} ${sortDir}`,
);
ORM 不是灵丹妙药——开发人员必须了解底层 SQL 行为。
存储过程和数据库强化
编写良好的带有参数的存储过程是安全的。但是,使用 EXEC 或 EXECUTE IMMEDIATE 的过程中的动态 SQL 会重新引入注入风险。
-- SAFE
CREATE PROCEDURE get_user(IN user_id INT)
BEGIN
SELECT * FROM users WHERE id = user_id;
END;
-- VULNERABLE
CREATE PROCEDURE get_user(IN table_name VARCHAR(64))
BEGIN
SET @sql = CONCAT('SELECT * FROM ', table_name);
PREPARE stmt FROM @sql; -- Dynamic SQL inside procedure
EXECUTE stmt;
END;
应用最小权限原则:应用程序数据库用户应该只拥有他们需要的权限 - 没有 DROP、ALTER 或 CREATE。使用 PostgreSQL 行级安全性、SQL Server EXECUTE AS 或 MySQL SQL SECURITY 定义者/调用者模式来强制在数据库层进行访问。
输入验证和 WAF 作为纵深防御
输入验证单独存在是不够的,但作为第二层是必不可少的。优先选择基于白名单的验证而不是黑名单过滤。类型强制(使用 parseInt 解析整数)、长度限制和 Unicode 规范化 (NFC/NFD) 可防止许多绕过尝试。
Web 应用程序防火墙是一种延迟策略,而不是解决方案。攻击者使用注释混淆 (/**/)、编码变体(URL、Unicode、十六进制、双 URL 编码)、HTTP 参数污染以及不太常见的 SQL 语法(例如 BENCHMARK() 而不是 SLEEP())来绕过 WAF。
-- WAF bypass: comment obfuscation
' UNION/**/SELECT/**/password/**/FROM/**/users--
-- WAF bypass: alternative functions
1 AND BENCHMARK(5000000, MD5('test')) -- time-based blind
NoSQL注入和自动扫描
NoSQL 数据库也容易受到攻击。 MongoDB $where 子句注入、盲提取的 $regex 注入以及 JSON 查询对象操作都会带来风险。该原则是通用的:任何连接用户输入的数据库语言都容易受到攻击。
| 数据库 | 注射矢量 | 防御 |
|---|---|---|
| MongoDB | $where、$regex、$ne | 使用 $eq,清理运算符 |
| 沙发床 | N1QL 字符串连接 | 参数化 N1QL |
| 卡桑德拉 | CQL 字符串连接 | 准备好的声明 |
| Redis | EVAL/EVALSHA 与 Lua | 参数化EVAL |
通过自动扫描左移。 Semgrep、ESLint 安全插件和 CodeQL 等 SAST 工具可检测拉取请求中的注入模式。 OWASP ZAP 等 DAST 工具可对正在运行的应用程序进行动态扫描。
# GitHub Actions workflow
name: Security Scan
on: pull_request
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npx @neutralinojs/semgrep --config=auto
dast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker run --network host owasp/zap2docker-stable zap-baseline.py -t http://localhost:3000
如果预防失败,请通过异常查询模式、意外错误和出站数据量峰值来检测注入。立即轮换凭证,通过查询日志分析识别注入点,遏制漏洞并保留取证证据。
通过一致应用已建立的模式,可以防止 SQL 注入。参数化查询是主要防御,具有原始查询感知的 ORM 提供结构保护、输入验证和 WAF 添加辅助层,CI/CD 中的自动扫描可验证合规性。一致地应用这些层,您就可以消除一整类漏洞。

