LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

如何从根本上防止 SQL 注入?

admin
2026年4月17日 14:52 本文热度 46
String sql = "SELECT * FROM users WHERE name = '" + userName + "'";

userName 传进来 ' OR '1'='1,拼出来的 SQL 变成 SELECT * FROM users WHERE name = '' OR '1'='1',条件恒真,整张表的数据全出来了。传个 '; DROP TABLE users; -- 进去,表直接没了。

解决办法在每本 Web 安全入门教材的同一页上:参数化查询。

PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, userName);

数据库先拿到 SQL 模板 SELECT * FROM users WHERE name = ?,编译好执行计划,结构锁死。之后填进去的 userName 无论是什么——单引号、分号、DROP TABLE——数据库都只当它是一个字符串值,不会当 SQL 语法去解析。

打个比方:字符串拼接像是把收件地址和包裹内容写在同一张纸上,快递员分不清哪部分是地址哪部分是内容。参数化查询是先递一张写好地址的快递单,然后再把包裹放上去——地址和内容从头就是分开的。

Java 的 PreparedStatement 从 JDK 1.1 就有了,1997 年的事。PHP 的 PDO、Python 的 DB-API、.NET 的 SqlCommand,每个语言都有。MyBatis 里 #{} 是参数化,${} 是拼接。Spring Data JPA 的 JPQL 默认就是参数化的。

工具齐了快三十年,但 OWASP 统计里注入类漏洞到现在还排前五。用了 ORM 的项目,首次安全扫描仍有 18% 检出 SQL 注入风险。

拿一个 MyBatis 项目举例,怎么排查自己有没有注入风险。全局搜 ${

grep -rn '${' src/main/resources/mapper/

搜出来的每一个 ${} 都要逐个过一遍。#{}  是安全的——MyBatis 会把它编译成 PreparedStatement 的 ? 占位符。${} 是直接拼进 SQL 字符串里的,跟手写字符串拼接没区别。

大部分 ${} 出现在两个地方:动态表名和 ORDER BY 排序字段。这两个参数化查询确实搞不定——表名和列名是 SQL 的结构部分,数据库编译执行计划之前就得知道它们是什么,? 占位符放不进去。

<!-- 这样写有注入风险 -->
<select id="queryByTable" resultType="map">
    SELECT * FROM ${tableName} WHERE id = #{id}
</select>

<!-- ORDER BY 也是 -->
<select id="listUsers" resultType="User">
    SELECT * FROM users ORDER BY ${sortColumn} ${sortOrder}
</select>

${tableName} 如果直接从前端参数里取,传个 users; DROP TABLE users; -- 进来就完了。${sortColumn} 传个 id; DELETE FROM users; -- 一样能打。

安全的做法是在 Java 层做白名单映射,不让前端传来的值直接进 SQL:

public String getSortColumn(String param) {
    switch (param) {
        case "name"return "user_name";
        case "date"return "created_at";
        defaultreturn "id";
    }
}

返回值只可能是代码里写死的那几个列名,前端传什么都不影响。动态表名同理——维护一个允许的表名集合,不在集合里的直接拒绝。

private static final Set<String> ALLOWED_TABLES = Set.of("orders""products""users");

public void query(String tableName) {
    if (!ALLOWED_TABLES.contains(tableName)) {
        throw new IllegalArgumentException("非法表名: " + tableName);
    }
    // 到这里 tableName 一定是安全的
}

搜完 ${},再搜 createNativeQuery 和 nativeQuery = true——这两个是 JPA/Hibernate 里写原生 SQL 的口子,走这条路的时候 ORM 不会帮你参数化。Django 项目搜 extra(raw(RawSQL,同样的问题。

ORM 的安全性建立在"你用它提供的查询接口"这个前提上。用 Criteria API、JPQL、QueryDSL 这些,框架帮你处理参数化。一旦绕过去写原生 SQL,就回到了手动管安全的状态。

那 SQL 标准能不能从语言层面把这件事堵死?

堵不死。SQL 就是一门语言,语言就意味着可以动态拼它。SELECT * FROM users WHERE name = ' + 用户输入 + '——SQL 解析器看到的只是一整条字符串,它分不清哪部分是程序员写的、哪部分是用户传的。

参数化查询能防注入,靠的不是"聪明地转义特殊字符"。是在协议层面把 SQL 结构和数据拆成了两次传输。SQL 模板先到数据库,编译好,结构锁死;数据后到,只能往槽里填,改不了结构。PostgreSQL 的协议里 Parse 和 Bind 就是两条独立消息,MySQL 的二进制协议也有类似的分离。这比转义靠谱得多——转义是在字符串层面做手脚,总有绕过的可能;协议层的分离是从根上断开的。

但前提是你得走 PreparedStatement 这条路。不走的话,数据库收到一整条拼好的 SQL,跟有没有注入风险它根本看不出来。grep -rn '${' src/ 这一行命令能帮你找到大部分没走这条路的地方。


该文章在 2026/4/17 17:36:47 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号