当前位置: 首页 > news >正文

Java代码审计实战:SQL注入漏洞挖掘与MyBatis安全编码规范

1. 项目概述:从开发者到审计者的视角转变

做Java开发这么多年,从写第一行SELECT * FROM users开始,就知道SQL注入是个“老生常谈”的安全问题。但真正让我对它有切肤之痛的,不是看漏洞报告,而是几年前自己写的一个内部管理系统被白帽子“教育”了。那个项目里,一个简单的订单查询功能,因为图省事在ORDER BY后面直接拼接了用户传入的排序字段,导致被拖了库。从那时起,我才真正明白,“知道”和“在代码里避免”是两回事。这也是为什么后来我开始深入研究Java代码审计,尤其是SQL注入——我想知道,一个看似功能正常的Java Web应用,它的“血管”(数据流)里到底藏着多少我们亲手埋下的“雷”。

所谓Java代码审计中的SQL注入审计,本质上是一场“猫鼠游戏”。开发者在业务逻辑的驱动下,追求灵活与效率,可能会在动态SQL、排序、模糊查询等场景下使用字符串拼接。而审计者(或攻击者)则像侦探一样,沿着数据从HTTP请求进入,经过Controller、Service、Dao层,最终到达SQL语句的完整路径,寻找任何一处可能的拼接点,并判断其是否可控、是否被有效过滤。这个过程不仅需要熟悉Java Web的典型架构(如Spring Boot + MyBatis),更需要理解SQL注入在各种上下文(JDBC、Hibernate、JPA、MyBatis)下的不同表现形式。今天,我就结合自己踩过的坑和审过的项目,把这套方法论和实操细节系统地梳理一遍,目标是让你看完后,不仅能快速上手审计,更能从根本上理解如何写出更安全的代码。

2. SQL注入原理与Java中的典型脆弱点

要审计,先得知道漏洞是怎么产生的。SQL注入的核心原因就一句话:将用户可控的数据,未经充分验证或转义,直接拼接到了SQL语句中,改变了原语句的语义。在Java生态里,这个“拼接”动作发生在不同层次,形态各异。

2.1 不同持久层框架下的注入模式

JDBC原生拼接:这是最原始、也最容易被发现的类型。特征非常明显:代码中存在用加号(+)或StringBuilder拼接字符串来构造SQL的语句。

// 典型的错误示例:直接拼接 String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql);

这里,如果username传入admin' --,整个SQL语义就被篡改了,密码验证被注释掉。审计时,全局搜索createStatement()executeQuery(executeUpdate(,然后查看其执行的SQL字符串是否包含拼接变量。

MyBatis中的${}#{}之辨:这是当前Java Web项目中最常见、也最隐蔽的注入点。MyBatis用#{}表示预编译占位符,用${}表示字符串替换。很多开发者,尤其是初学者,并不清楚两者的本质区别,甚至在官方文档不显眼的地方才有警告。

<!-- 安全:预编译,注入无效 --> <select id="findUser" parameterType="String" resultType="User"> SELECT * FROM user WHERE username = #{name} </select> <!-- 危险:直接替换,存在注入风险 --> <select id="findUser" parameterType="String" resultType="User"> SELECT * FROM user WHERE username = '${name}' </select>

#{name}在运行时会被替换成?,然后通过PreparedStatement.setString()等方法安全地设置参数值。而${name}则是在SQL解析阶段,直接进行字符串替换。如果name的值是admin' OR '1'='1,替换后SQL语法依然正确,但逻辑被绕过。审计MyBatis项目,核心就是全局搜索${,尤其是在Mapper XML文件中。

Hibernate与JPA的误区:很多人认为使用ORM框架就高枕无忧了,这是致命的误解。Hibernate的HQL(Hibernate Query Language)和JPA的JPQL(Java Persistence Query Language)如果使用字符串拼接,同样会导致注入,这类注入通常被称为“HQL注入”或“JPQL注入”。

// 错误:HQL拼接 String hql = "FROM User WHERE username = '" + username + "'"; Query query = session.createQuery(hql); // 正确:使用参数绑定 String hql = "FROM User WHERE username = :username"; Query query = session.createQuery(hql); query.setParameter("username", username);

ORM框架防止的是SQL注入,因为它最终生成的SQL是参数化的。但如果你拼接的是HQL/JPQL字符串,框架会将其完整地作为查询语言解析,攻击者同样可以注入HQL语句(例如通过' OR '1'='1绕过)。审计时需关注createQuery(createNativeQuery((原生SQL拼接风险更高)等方法调用。

2.2 那些容易被忽略的“合法”拼接场景

并不是所有使用${}或字符串拼接的地方都立刻判“死刑”。有些场景下,使用#{}会导致语法错误,迫使开发者使用${},这就成了风险集中的高地。审计时需要特别关注以下四个高危场景:

  1. 动态表名/字段名:SQL语法不允许预编译占位符(?)出现在表名或字段名位置。SELECT * FROM #{tableName}预编译后会变成SELECT * FROM ?,执行时会报错。因此,当功能需要动态选择表或字段时,开发者往往被迫使用${}。这里的风险在于,如果tableName参数用户可控(比如通过前端下拉框传入,但被恶意篡改),就可能注入UNION SELECT等子句。

  2. ORDER BY / GROUP BY 子句:与表名类似,ORDER BY #{field}会被编译为ORDER BY 'field',导致按字符串常量排序,而非按字段排序,不符合预期。因此,动态排序功能常使用ORDER BY ${sortField} ${sortOrder}。如果sorField可控,攻击者可以传入id,(SELECT SLEEP(5))这类语句进行盲注。

  3. LIKE 模糊查询:这是一个经典误区。有人尝试LIKE '%#{keyword}%',但预编译后是LIKE '%?%',数据库会将?连同百分号一起视为一个字符串参数,导致查询失败。错误的做法是使用LIKE '%${keyword}%',这就敞开了注入的大门。正确的做法是在SQL中使用数据库的字符串连接函数配合#{},如MyBatis中:LIKE CONCAT('%', #{keyword}, '%')

  4. IN 语句:直接写IN (#{ids})是不行的,因为预编译期望一个参数,但你传入的是一个列表。MyBatis提供了<foreach>标签来安全处理,但有些开发者会错误地拼接字符串:IN (${idList}),如果idList来自用户输入(如1,2,3) OR 1=1 --),注入就产生了。

注意:审计时看到这些场景使用了${},要立刻提高警惕。但这只是第一步,关键还要看这个${}中的参数来源是否用户可控是否被严格过滤或白名单校验

3. 代码审计实战:四步定位SQL注入漏洞

理论清楚了,我们进入实战。假设拿到一个Spring Boot + MyBatis的项目代码,如何系统性地进行SQL注入审计?我总结为“四步定位法”。

3.1 第一步:全局扫描,锁定可疑点

工具先行,提高效率。可以使用grepfind命令,或者IDE的全局搜索功能(IntelliJ IDEA的Ctrl+Shift+F非常强大)。

搜索关键词清单

  • XML文件(MyBatis Mapper)\$\{。这是最高效的方式,能直接定位到MyBatis中所有字符串替换点。
  • Java代码中的SQL字符串\.executeQuery\(\.executeUpdate\(\.createStatement\(\+.*['"](粗糙但广泛)、StringBuilder.*append.*SELECT"SELECT.*\+"
  • Hibernate/JPAcreateQuery\(.*\+createNativeQuery\(.*\+
  • 注解形式SQL(MyBatis @Select等):同样搜索\$\{,因为注解中也可以使用。

例如,在项目根目录下执行:

# 查找所有Mapper XML中的${} find . -name "*.xml" -type f | xargs grep -l "\${" | grep -i mapper # 查找Java代码中的字符串拼接SQL find . -name "*.java" -type f | xargs grep -n "\.executeQuery\("

3.2 第二步:逆向追踪,绘制数据流图

找到可疑点(比如一个${keyword})后,这只是漏洞的“终点”。我们需要逆向追踪,找到这个参数的“起点”,即它从哪里来。这个过程就像侦探破案,追踪资金的流向。

以MyBatis Mapper为例,标准追踪路径是:Mapper XML->Mapper Interface->Service Impl->Controller->HTTP Request

  1. 从Mapper XML开始:假设在UserMapper.xml中发现SELECT * FROM user WHERE name LIKE '%${name}%'
  2. 找到对应的Mapper接口:查看XML文件头部的namespace,如com.example.dao.UserMapper。找到该Java接口,里面会有一个方法名与XML中selectid对应,例如List<User> findUsersByName(String name);
  3. 找到Service层调用:在IDE中,右键点击这个findUsersByName方法,选择“Find Usages”(查找引用)。通常会跳转到UserServiceImpl类中的一个方法。
  4. 分析Service方法:查看该Service方法的实现。参数name是从哪里来的?可能是直接传入,也可能是从某个DTO对象中获取。继续向上追踪。
  5. 找到Controller层:再次使用“Find Usages”查找该Service方法的调用处,最终会定位到某个@RestController@Controller中的方法,该方法通常带有@GetMapping@PostMapping等注解。
  6. 确认参数来源:查看Controller方法的参数。它可能使用了@RequestParam("keyword") String keyword@RequestBody UserQueryDTO dto。至此,你确认了参数name最终来源于用户HTTP请求。

追踪过程中的关键判断点

  • 参数类型:如果Mapper中${}对应的接口方法参数是intIntegerLong等数字类型,风险相对较低(但仍需警惕数字型注入,虽然罕见)。如果是String,风险陡增。
  • 中间处理:在Service层或Controller层,是否对参数进行了过滤、转义或校验?例如,是否调用了StringEscapeUtils.escapeSql(注意:这个方法是不推荐用于防SQL注入的!它只为JDBC转义,并非万无一失)、是否使用了正则表达式过滤了单引号、分号等?过滤逻辑是否严谨,能否被绕过(如双写、编码绕过)?
  • 全局过滤器/拦截器:查看项目是否有配置FilterInterceptorAOP切面,对请求参数进行全局的SQL注入过滤。例如,是否过滤了sleepbenchmarkunion select等关键字。但要注意,这种黑名单方式很容易被绕过。

3.3 第三步:上下文分析,评估真实风险

并非所有拼接都意味着立即可利用的漏洞。需要结合上下文进行深度分析。

场景一:动态排序(ORDER BY)

<select id="findUsers" resultType="User"> SELECT * FROM user ORDER BY ${sortField} ${sortOrder} </select>
  • 风险:极高。sortFieldsortOrder通常直接来自前端排序控件。
  • 审计:追踪sortField。如果前端固定传入idname等字段名,且后端没有映射机制,攻击者可以修改请求,将sortField设置为id,(SELECT IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)),即可进行基于时间的盲注。
  • 安全方案:应使用白名单校验。在后端维护一个允许排序的字段列表,将前端传入的值与白名单比对,或用枚举限定。
// 安全做法:白名单映射 private static final Map<String, String> SORT_FIELD_WHITELIST = new HashMap<>(); static { SORT_FIELD_WHITELIST.put("createTime", "create_time"); SORT_FIELD_WHITELIST.put("userName", "username"); } String dbField = SORT_FIELD_WHITELIST.getOrDefault(userInputField, "id"); // 默认值 // 然后将dbField传入Mapper,此时使用${}风险可控,因为值已被限定

场景二:模糊查询(LIKE)

<select id="search" resultType="Item"> SELECT * FROM items WHERE title LIKE '%${keyword}%' </select>
  • 风险:高。keyword直接来自搜索框。
  • 审计:这是最常见的注入点之一。需要检查Service层是否对keyword做了处理。即便做了trim()或简单的替换空格,也防不住注入。
  • 安全方案:必须使用CONCAT函数或bind标签。
<!-- 方案1:使用CONCAT --> <select id="search" resultType="Item"> SELECT * FROM items WHERE title LIKE CONCAT('%', #{keyword}, '%') </select> <!-- 方案2:使用bind标签(MyBatis) --> <select id="search" resultType="Item"> <bind name="pattern" value="'%' + keyword + '%'" /> SELECT * FROM items WHERE title LIKE #{pattern} </select>

实操心得bind标签创建的pattern变量,在内部也是通过预编译处理的,所以是安全的。这比在Java代码中拼接好%keyword%字符串再传给#{}更清晰。

场景三:IN语句错误做法:SELECT * FROM items WHERE id IN (${ids})。 安全做法:使用MyBatis的<foreach>标签遍历集合。

<select id="findByIds" resultType="Item"> SELECT * FROM items WHERE id IN <foreach collection="idList" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

审计时,如果发现IN语句使用${}拼接,且ids是一个由用户输入的逗号分隔字符串转换而来的列表,风险极高。

3.4 第四步:验证与利用链构造

在代码层面确认存在风险后,如果条件允许(如测试环境),需要构造利用链进行验证。

  1. 确定注入类型:是字符型(参数被引号包裹)还是数字型(无引号)?这决定了Payload的构造方式。例如WHERE id = ${id}是数字型,WHERE name = '${name}'是字符型。
  2. 测试闭合:对于字符型,首先测试能否闭合引号。传入name=test',观察应用是否报错(数据库语法错误)。如果报错,说明注入存在。
  3. 信息探测:尝试使用UNION SELECT查询数据。需要判断列数,例如:name=test' UNION SELECT 1,2,3 --。如果页面正常显示并出现了数字2或3,说明该位置可以回显数据。
  4. 盲注测试:如果无回显,尝试基于布尔或时间的盲注。例如:
    • 布尔盲注:name=test' AND SUBSTRING(database(),1,1)='a' --,通过页面内容差异判断真假。
    • 时间盲注:name=test' AND IF(1=1, SLEEP(5), 0) --,观察响应是否延迟。
  5. 利用工具:可以使用sqlmap进行自动化验证,但前提是已获得测试授权。命令如:sqlmap -u "http://target.com/search?keyword=*" --batch

注意事项:在真实审计或渗透测试中,必须在授权范围内进行。未经授权的测试是违法行为。

4. 深入MyBatis:#{}${}的底层差异与安全边界

很多开发者对MyBatis中#{}${}的区别停留在“一个安全一个不安全”的层面,但知其然更要知其所以然。理解底层原理,才能更好地审计和编码。

4.1 预编译(PreparedStatement)是如何工作的?

当使用#{}时,MyBatis会创建一个PreparedStatement对象。SQL语句在发送到数据库之前就被编译了,语法结构已经固定。#{}会被替换成一个占位符?。后续传入的参数,无论是什么内容,都只会被当作数据(而不是代码)传递给这个已编译的语句。

// MyBatis(近似)底层做的事 String sql = "SELECT * FROM user WHERE id = ?"; // SQL已编译 PreparedStatement ps = connection.prepareStatement(sql); ps.setInt(1, userId); // 安全地设置参数 ResultSet rs = ps.executeQuery();

数据库知道?的位置应该是一个整数类型的值,因此即使你传入1 OR 1=1setInt方法也会将其强制转换为整数(可能失败或转换为1),或者直接将其视为一个完整的字符串值,而不会将其解析为SQL语法的一部分。这就从根本上杜绝了注入。

4.2 字符串替换(${})的风险本质

${}是在SQL语句编译之前就进行简单的字符串替换。你可以把它想象成Java中的字符串拼接。

// 假设 userId = "1 OR 1=1" String sql = "SELECT * FROM user WHERE id = " + userId; // 拼接后:SELECT * FROM user WHERE id = 1 OR 1=1 Statement stmt = connection.createStatement(); // 创建Statement ResultSet rs = stmt.executeQuery(sql); // 执行拼接后的SQL

替换后的完整SQL字符串被送到数据库编译执行。如果其中包含了SQL关键字和语法,数据库就会忠实执行。这就是注入发生的根本原因。

4.3 那些“安全”的${}使用场景

审计时,我们也会遇到一些使用了${}但风险极低或可控的情况,不要误报。

  1. 硬编码值或常量
    ORDER BY ${@com.example.constant.SortConstant@DEFAULT_FIELD}
    这里的值来自一个静态常量,用户无法控制。
  2. 经过严格白名单校验的参数:如前文所述,动态排序字段经过白名单映射后,传入${}的值仅限于idname等预定义的几个,风险可控。
  3. 数字类型且业务逻辑强校验的参数:例如,分页参数${pageNum},如果业务逻辑确保它只能是大于0的整数(通过Integer.parseInt并判断>0),那么注入空间也很小(但数字型注入理论存在,需结合业务看)。

审计策略:看到${},不要立刻标记为漏洞。必须完成逆向追踪,确认该参数的源头是否用户可控,以及在到达此处之前是否经过了有效的、不可绕过的安全处理。如果源头不可控或处理有效,则可以放行。

5. 进阶审计技巧与常见盲点

除了常规的CRUD操作,一些复杂的业务场景或框架特性会隐藏更深的注入点。

5.1 MyBatis动态SQL标签中的陷阱

MyBatis的<if><choose><when><otherwise>等动态SQL标签非常强大,但使用不当也会引入风险。

<select id="findUser" parameterType="UserQueryDTO" resultType="User"> SELECT * FROM user WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE '%${name}%' <!-- 危险!在动态标签内使用了${} --> </if> <if test="orderBy != null"> ORDER BY ${orderBy} <!-- 危险! --> </if> </select>

审计要点:检查动态SQL标签内部使用的表达式。test表达式中的nameorderBy是OGNL表达式,引用的是传入的参数对象属性。如果其中直接使用了${}拼接,风险同样存在。需要追踪UserQueryDTOnameorderBy属性的来源。

5.2 注解式SQL的审计

MyBatis也支持在Mapper接口方法上直接使用@Select@Update等注解编写SQL。审计方式与XML类似。

@Select("SELECT * FROM user WHERE username = '${username}'") // 危险! User findByUsername(@Param("username") String username); @Select("SELECT * FROM user WHERE username = #{username}") // 安全 User findByUsernameSafe(@Param("username") String username);

全局搜索@Select(@Update(等注解,检查其中的SQL字符串是否包含拼接。

5.3 批量操作与复杂嵌套查询

在批量插入、更新,或者多层子查询中,开发者可能为了性能或灵活性而使用拼接。

<insert id="batchInsert"> INSERT INTO user (name, age) VALUES <foreach collection="userList" item="user" separator=","> ('${user.name}', ${user.age}) <!-- 危险!user.name是字符串 --> </foreach> </insert>

这里虽然用了<foreach>,但内部值仍用${}拼接,应改为#{user.name}#{user.age}

5.4 框架自动生成代码的坑

如MyBatis Generator或类似工具生成的代码,通常默认使用#{}。但后续开发者在手动修改功能时,可能会无意中将#{}改为${},如上文参考案例中的LIKE查询。审计时,对于自动生成的Mapper文件,要重点关注那些与默认生成模式不同的地方,尤其是手写修改过的部分。

6. 修复方案与安全编码规范

审计的最终目的不仅是发现问题,更是推动修复。针对发现的SQL注入点,应提供明确、可操作的修复建议。

6.1 优先使用预编译(#{}

这是铁律。99%的场景都应该使用#{}

6.2 必须使用${}时的安全措施

对于表名、字段名、排序等场景,必须采用白名单机制。

示例:安全的动态ORDER BY实现

@Service public class UserService { private static final Set<String> ALLOWED_SORT_FIELDS = Set.of("id", "create_time", "username"); private static final Set<String> ALLOWED_ORDERS = Set.of("ASC", "DESC"); public List<User> findUsers(String sortField, String sortOrder) { // 1. 白名单校验 if (!ALLOWED_SORT_FIELDS.contains(sortField)) { sortField = "id"; // 默认值 } if (!ALLOWED_ORDERS.contains(sortOrder.toUpperCase())) { sortOrder = "ASC"; } // 2. 可选的额外过滤:移除非字母数字下划线字符(防御性编程) // sortField = sortField.replaceAll("[^a-zA-Z0-9_]", ""); // 3. 将校验后的安全参数传入Mapper return userMapper.findUsersWithOrder(sortField, sortOrder); } }
<!-- Mapper中可以使用${},因为参数已受控 --> <select id="findUsersWithOrder" resultType="User"> SELECT * FROM user ORDER BY ${safeSortField} ${safeSortOrder} </select>

6.3 LIKE模糊查询的正确姿势

绝对不要使用LIKE '%${value}%'正确做法

<!-- MySQL --> <select id="search" resultType="Item"> SELECT * FROM items WHERE title LIKE CONCAT('%', #{keyword}, '%') </select> <!-- 或使用bind标签(数据库无关) --> <select id="search" resultType="Item"> <bind name="pattern" value="'%' + keyword + '%'" /> SELECT * FROM items WHERE title LIKE #{pattern} </select>

6.4 IN语句的正确写法

使用MyBatis的<foreach>标签。

<select id="findByIds" resultType="Item"> SELECT * FROM items WHERE id IN <foreach collection="idList" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

确保传入的idList是一个List<Integer>List<Long>,而不是逗号分隔的字符串。在Service层就做好转换和校验。

6.5 全局防御的局限性

有些项目会引入过滤器或拦截器,对请求参数中的SQL关键字进行过滤或转义。但这只能作为辅助手段,绝不能替代参数化查询。

  • 黑名单过滤:容易绕过。如SLEEP(5)可以写成SLEEP/**/(5)SLEEP(5)(用%00空字节)、或使用编码。
  • StringEscapeUtils.escapeSql:这是Apache Commons Lang的一个方法,但它仅转义少数字符(如单引号变两个单引号),对于数字型注入或没有引号的注入无能为力,且并非所有数据库都适用。结论参数化查询(预编译)是唯一公认的、根本的解决方案。其他方法都应在充分评估风险后,作为补充措施。

7. 自动化审计工具辅助与人工审计的平衡

完全依赖工具或完全人工审计都是低效的。应该结合使用。

  1. 静态代码分析工具(SAST)

    • SonarQube:可以配置规则检测Java代码中的SQL拼接(如发现Statement.executeQuery拼接字符串)、MyBatis Mapper中的${}使用。
    • FindBugs/SpotBugs:有规则能检测JDBC相关的注入问题。
    • 专有工具:如Fortify SCA、Checkmarx等商业工具,对Java SQL注入的检测规则比较成熟。
    • 局限性:工具会产生大量误报(如把安全的${}常量也报出来)和漏报(尤其是经过多层封装的、逻辑复杂的注入点)。它只能作为初步筛查,所有报告必须经过人工复核。
  2. 人工审计的核心价值

    • 理解业务上下文:工具不知道${sortField}是否经过了白名单校验,但人工追踪代码可以。
    • 分析复杂数据流:参数可能经过AOP处理、多个Service方法转换、从Session或缓存中获取,工具很难完整追踪。
    • 识别逻辑漏洞:工具主要找语法模式,而人工能发现“数字型参数在特定业务逻辑下可能被利用”这类更深层的问题。

推荐的审计流程

  1. 使用SAST工具对全量代码进行扫描,生成初步报告。
  2. 根据报告,优先审查高危漏洞点(如Mapper中的${}、Java中的字符串拼接SQL)。
  3. 针对核心业务模块(如用户管理、订单查询、搜索功能)进行人工“代码走查”,尤其关注接收外部参数的入口方法。
  4. 对发现的问题点,严格遵循“逆向追踪数据流”的方法进行确认。
  5. 编写审计报告,清晰描述漏洞位置、风险数据流、利用方式、修复建议。

8. 实战案例复盘:一个电商项目的SQL注入挖掘

去年我审计过一个开源的Spring Boot电商项目,就遇到了一个非常典型的、多层封装的SQL注入案例,它完美展示了审计的完整链条。

漏洞发现

  1. 全局扫描:使用grep -r "\${" --include="*.xml" .,在商品搜索的Mapper文件GoodsMapper.xml中发现:

    <select id="searchGoods" resultType="Goods"> SELECT * FROM goods WHERE 1=1 <if test="keyword != null and keyword != ''"> AND (goods_name LIKE '%${keyword}%' OR goods_desc LIKE '%${keyword}%') </if> <if test="orderBy != null"> ORDER BY ${orderBy} </if> </select>

    两个高危点:LIKE '%${keyword}%'ORDER BY ${orderBy}

  2. 逆向追踪

    • 找到GoodsMapper接口中的searchGoods方法,参数是一个Map<String, Object>
    • GoodsServiceImpl中找到调用,发现keywordorderBy都是从传入的SearchDTO对象中获取。
    • 追踪到GoodsController,发现一个/goods/search的接口,使用@RequestBody SearchDTO dto接收JSON参数。SearchDTO中有keywordorderBy字段。
    • 关键发现:在Controller和Service层,没有对这两个字段进行任何过滤或校验!
  3. 风险确认

    • keyword是字符串,直接用于LIKE拼接,存在明显的字符型注入。
    • orderBy也是字符串,用于ORDER BY拼接,存在注入可能。
  4. 构造利用

    • 启动本地测试环境。
    • 发送POST请求到/goods/search,Body为:{"keyword": "test' AND '1'='1", "orderBy": "id"}
    • 观察日志,发现执行的SQL为:... WHERE 1=1 AND (goods_name LIKE '%test' AND '1'='1%' ...,由于单引号被闭合,AND '1'='1成为永真条件,成功注入。
    • 进一步,可以构造keywordtest' UNION SELECT 1,2,database(),4,5 --来获取数据库名。
  5. 修复建议

    • LIKE语句改为LIKE CONCAT('%', #{keyword}, '%')
    • orderBy建立白名单:List<String> allowedFields = Arrays.asList("id", "price", "create_time");,校验传入值是否在白名单内,不在则使用默认值。

这个案例的教训是:即使项目使用了MyBatis这样的半自动化框架,如果开发者不了解${}#{}的安全差异,且缺乏必要的输入校验,依然会制造出严重的漏洞。代码审计的价值,就在于发现这些隐藏在“便捷”功能背后的安全债务。

9. 总结与个人体会

干了这么多年开发和审计,我最大的体会是:安全是一种习惯,而不是一项功能。SQL注入这种“古老”的漏洞之所以经久不衰,不是因为技术有多难防,而是因为开发者在追求功能、赶进度时,最容易牺牲的就是那些“不起眼”的安全细节。

对于开发者,我的建议是:把“使用#{}”刻在肌肉记忆里。每当你要写SQL,手碰到键盘,第一个反应就应该是“这里能不能用#{}?”。如果不能用,立刻警铃大作,然后去查文档、问同事,寻找安全的替代方案(如白名单、CONCAT函数、<foreach>标签)。不要心存侥幸,攻击者不会因为你的业务逻辑简单就放过你。

对于审计者或安全工程师,我们的角色更像是“代码医生”和“布道者”。审计时,要像侦探一样耐心、细致,不放过任何一条数据流。报告问题时,不能只说“这里有SQL注入,高危”,更要清晰地描述攻击路径、提供可立即执行的修复代码,甚至最好能给团队做一次简短的培训,解释为什么${}危险,以及安全的做法是什么。推动修复的过程,也是提升整个团队安全水位的过程。

最后,工具永远在迭代,攻击手段也在翻新,但安全的核心原则——不信任任何用户输入,对输入进行校验、过滤,对输出进行编码,在操作数据时使用参数化查询——是永恒不变的。把这些原则内化到日常开发中,我们才能从源头上减少漏洞的产生,写出更健壮、更可靠的代码。

http://www.jsqmd.com/news/1130060/

相关文章:

  • 基于图像识别的游戏自动化架构:鸣潮后台智能操作技术实践
  • 如何解决SickGear常见问题:从连接错误到下载失败的终极指南
  • Soft-NMS 与 DIoU-NMS 实战:在 YOLOv8 中提升密集目标 5% mAP
  • 零基础快速上手Linly-Talker:数字人对话系统完整指南
  • 一键安装BetterNCM插件:告别繁琐操作,3分钟增强网易云音乐体验
  • 3步轻松获取国家中小学智慧教育平台电子课本:开源下载工具完整指南
  • DC-DC降压转换与STM32控制:硬件选型与I2C通信实践
  • 终极指南:如何用Python免费读取通达信数据,开启量化分析新时代
  • 如何高效解决3大流媒体下载难题:N_m3u8DL-RE终极方案
  • 如何实现个人数据的完全掌控:WeChatMsg微信聊天记录本地化解析方案
  • 猫抓浏览器资源嗅探插件:免费高效的网页媒体下载终极指南
  • 深度解析:如何高效部署Shopware 6电商平台的完整实践指南
  • 为什么你的鸣潮游戏时间总是不够用?这3个秘密让AI帮你自动刷图
  • 3分钟掌握Aria2GUI:macOS上的专业级下载管理解决方案
  • 如何为金融市场构建Kronos预测模型:从基础模型到实际部署的完整指南
  • 如何彻底掌控你的微信聊天记录:WeChatMsg终极数据管理方案
  • ItChat-UOS插件开发:如何扩展自定义功能的详细教程
  • 国家中小学智慧教育平台电子课本下载终极指南:5分钟掌握免费PDF下载技巧
  • 国家中小学智慧教育平台电子课本解析工具:三步实现教材PDF批量下载
  • LX Music音源聚合终极指南:如何构建你的全平台无损音乐库
  • 三步极速下载国家中小学智慧教育平台电子课本:免费PDF获取终极方案
  • SELinux安全增强Linux:从核心概念到实战权限问题解决
  • 如何实现Mac与Windows无缝文件共享?开源NTFS工具的3种解决方案
  • 解锁Shopware 6:企业级电商平台的深度配置与性能优化指南
  • SingleShotPose项目详解:CVPR 2018论文背后的革命性姿态估计算法
  • 解密PowerRemoteDesktop:PowerShell远程桌面架构革命与技术突破
  • 如何永久保存微信聊天记录:WeChatMsg完全免费的数据备份终极指南
  • 从零复现GitHub深度学习项目:环境配置、代码解读与实战避坑指南
  • 3分钟让普通鼠标在macOS上变得比触控板更强大:终极免费工具指南
  • 3个核心问题解决指南:如何用ok-ww让鸣潮日常任务从耗时变高效