SQL 注入防护

默认使用参数化语句或 ORM 绑定;动态标识符白名单化;数据库账户按应用最小权限拆分。

禁止将用户输入拼进 SQL 字符串;必要时仅允许从固定集合映射表名/列名。存储过程若拼接同样危险。审查清单应覆盖所有 executeraw、字符串模板与动态 ORDER BY

二阶注入:入库时未约束、出库后再拼查询 — 全程参数化才能闭环。LIKE 用户片段需转义 %_。错误信息不应对外暴露数据库细节。

数据访问安全流(请求 → 绑定 → 执行)

  [ HTTP 请求体 / 查询串 / Header ]
        │
        ▼
  ┌─────────────┐     校验类型、长度、枚举;拒绝裸字符串进 SQL
  │  输入规范化   │
  └─────────────┘
        │
        ▼
  ┌─────────────┐     占位符 / 命名参数;禁止「引号 + 拼接」
  │ 参数化绑定    │──── 动态表名列名:仅白名单映射,不用用户原串
  └─────────────┘
        │
        ▼
  ┌─────────────┐     最小权限账号;审计与慢查询监控
  │ 驱动执行 SQL │──── 异常:统一错误页,日志落盘含 request id
  └─────────────┘

Agent 生成数据访问代码时,默认模板应带占位符;若出现字符串拼接或模板插值插进 SQL,视为须修复项。

参数化查询 — 3 种语言实现

由驱动/协议把「语句结构」与「数据值」分离:占位符在服务端解析,用户输入只作为绑定值传递,不参与关键字解析。

Node.js — node-postgres (pg)

// ❌ 危险:字符串拼接
const sql = `SELECT * FROM users WHERE email = '${req.body.email}'`;
await client.query(sql);

// ✅ 安全:$1 占位符参数化
const result = await client.query(
  'SELECT id, name FROM users WHERE email = $1 AND active = $2',
  [req.body.email, true]
);

// ✅ 批量 INSERT(避免循环拼接)
const values = users.map((_, i) => `($${i*2+1}, $${i*2+2})`).join(',');
await client.query(
  `INSERT INTO users (name, email) VALUES ${values}`,
  users.flatMap(u => [u.name, u.email])
);

Python — psycopg2

# ❌ 危险:% 字符串格式化
cursor.execute("SELECT * FROM orders WHERE user_id = %s" % user_id)

# ✅ 安全:参数元组(第二个参数)
cursor.execute(
    "SELECT id, amount FROM orders WHERE user_id = %s AND status = %s",
    (user_id, "active")
)

# ✅ LIKE 安全转义
search = request.args.get("q", "")
safe_search = search.replace("%", "\\%").replace("_", "\\_")
cursor.execute(
    "SELECT * FROM products WHERE name ILIKE %s ESCAPE '\\'",
    (f"%{safe_search}%",)
)

Java — JDBC PreparedStatement

// ❌ 危险:Statement + 拼接
String sql = "SELECT * FROM accounts WHERE id = " + id;
Statement st = conn.createStatement();
st.executeQuery(sql);

// ✅ 安全:PreparedStatement
String sql = "SELECT balance FROM accounts WHERE id = ? AND owner_id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setLong(1, accountId);
    ps.setLong(2, currentUserId);
    ResultSet rs = ps.executeQuery();
    // ...
}
  • 存储过程参数:若过程内部仍拼接动态 SQL,参数化外层挡不住 — 须审查过程体。
  • 批量操作:用批量绑定 API,避免循环里拼 IN (...) 长串。

ORM 危险用法与安全替代(Prisma 示例)

// ❌ 危险:Prisma $queryRaw 直接插值(模板字符串)
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE role = '${userInput}'
`;
// 注意:即使用了模板字面量,如果拼接 JS 变量则不安全!
const unsafeQuery = `SELECT * FROM users WHERE name = '${name}'`;
await prisma.$queryRaw(Prisma.raw(unsafeQuery));  // ❌ 危险

// ✅ 安全:Prisma $queryRaw 使用 Prisma.sql 标签(自动参数化)
import { Prisma } from '@prisma/client';

const users = await prisma.$queryRaw(
  Prisma.sql`SELECT id, name FROM users WHERE role = ${userRole} AND active = ${true}`
);

// ✅ 更安全:使用 Prisma 类型安全 API(完全避免原生 SQL)
const users = await prisma.user.findMany({
  where: { role: userRole, active: true },
  select: { id: true, name: true },
});

// ✅ 安全的动态排序(白名单映射)
const ALLOWED_SORT_FIELDS = {
  'name': 'name',
  'createdAt': 'created_at',
  'score': 'score',
} as const;

const sortField = ALLOWED_SORT_FIELDS[req.query.sort as string];
if (!sortField) throw new Error('Invalid sort field');

const users = await prisma.$queryRaw(
  Prisma.sql`SELECT * FROM users ORDER BY ${Prisma.raw(sortField)} ASC`
);
  • 原生片段:仅允许团队审核过的静态片段 + 绑定参数,不把整段查询交给用户或模型自由生成。
  • 连接池与方言:注意转义与标识符引用差异;迁移脚本不等同于应用内防注入策略。

最小权限配置 SQL 与注入攻击向量防御

数据库用户最小权限配置

-- 创建应用专用用户(PostgreSQL)
CREATE USER app_readonly WITH PASSWORD 'strong-password-here';
CREATE USER app_writer WITH PASSWORD 'another-strong-password';

-- 只读用户:仅 SELECT
GRANT CONNECT ON DATABASE mydb TO app_readonly;
GRANT USAGE ON SCHEMA public TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO app_readonly;

-- 写入用户:仅 INSERT/UPDATE/SELECT,禁止 DDL
GRANT CONNECT ON DATABASE mydb TO app_writer;
GRANT USAGE ON SCHEMA public TO app_writer;
GRANT SELECT, INSERT, UPDATE ON TABLE orders, products TO app_writer;
REVOKE DELETE ON ALL TABLES IN SCHEMA public FROM app_writer;

-- 禁止 FILE 等高危权限(MySQL)
REVOKE FILE ON *.* FROM 'app_user'@'%';
REVOKE PROCESS ON *.* FROM 'app_user'@'%';
REVOKE SUPER ON *.* FROM 'app_user'@'%';

三种注入向量与防御代码

-- ① UNION 注入防御:永远不动态拼接 SELECT 字段列表
-- ❌ 危险:SELECT ${userFields} FROM users WHERE id = $1
-- ✅ 安全:字段硬编码,id 参数化
SELECT id, name, email FROM users WHERE id = $1;

-- ② 时间盲注防御(pg_sleep/SLEEP 检测)
-- 防御:超时配置(statement_timeout),阻止长时间运行的查询
SET statement_timeout = '5s';  -- PostgreSQL per-session

-- ③ 二阶注入防御:写入与读出两处都参数化
-- 存储用户输入的 username(写入时参数化)
INSERT INTO profiles (user_id, username) VALUES ($1, $2);
-- 再次使用 username 时仍参数化(不假设库里的数据已安全)
SELECT * FROM audit_log WHERE username = $1;
  • 运维:只读副本禁 DDL;应用账号禁 FILE 等高危权限;结合 SAST 与 CI 规则覆盖数据访问层。

危险模式高亮(SQL 片段实验室)

下方编辑区为「透明文字 + 底层高亮」叠层:在框内直接输入或粘贴代码/SQL 混合片段,将标出常见注入相关写法(示意教学,不能替代静态分析或人工审计)。

检测包含:引号后拼接、模板插值 ${...}.format(、恒真条件 OR 1=1、注释截断 '--、多语句 ; DROP 等模式。

高亮为启发式匹配,可能误报;真正修复方式是占位符绑定、白名单化动态标识符,并审计所有原生 SQL 入口。

---
name: sql-injection-prevention
description: 审查数据访问层防注入与权限,含参数化查询、ORM 陷阱、最小权限配置
---
# 步骤
1. 扫描所有 db.query / execute / raw:禁止字符串拼接或模板插值
2. Node.js pg:用 $1/$2 占位符;Python psycopg2:用 %s 参数元组
3. Java:用 PreparedStatement,不用 Statement + 拼接
4. Prisma ORM:禁止 Prisma.raw(userInput);用 Prisma.sql 标签或类型安全 API
5. 动态排序字段:用白名单映射字典(Map),不用 ORDER BY ${userInput}
6. LIKE 查询:对 % 和 _ 做转义,使用 ESCAPE 子句
7. 二阶注入:写入路径和读出路径均参数化,不假设库中数据已安全
8. DB 权限:应用账号仅 SELECT/INSERT/UPDATE,禁止 DDL/FILE/SUPER
9. 错误信息:生产环境不暴露 SQL 错误、表名、列名
10. UNION/盲注防御:设置 statement_timeout 防止时间盲注
11. 存储过程体:审查过程内部是否仍有 EXEC/sp_executesql 拼接
12. CI 检测:semgrep 规则 p/sql-injection 在 PR 中运行

# 禁止
- 禁止将用户输入拼进任何 SQL 字符串
- 禁止 ORDER BY ${userColumn} 或等价动态标识符
- 禁止对外暴露数据库错误详情

返回技能库 更多技能入口