从SQL注入到参数化查询:SRM系统receiptDetail接口安全修复全记录

发布时间:2026/10/21 23:26:46

从SQL注入到参数化查询:SRM系统receiptDetail接口安全修复全记录 把 SQL 拼接换掉之后很多同事问我同一个问题为什么一个接口参数没处理好就能让整个采购系统的收货数据裸奔出来。其实问题的关键不在于攻击手法有多高明而在于 SQL 语句的生成方式从一开始就走错了路。下面这篇文章是我整理的一份完整记录包含漏洞成因、本地复现验证思路、POC 设计定位以及最终修复和回归检测的全过程希望能给正在排查同类问题的读者一点参考。SRM 智联云采系统的 receiptDetail 接口负责按收货单维度查询明细数据。这个接口在收货列表、详情展示、对账导出等多个场景都会被调用属于典型的高频数据出口。而我正是在评估该接口时发现它存在一个可被利用的 SQL 注入漏洞。为了方便复盘我把整个分析过程拆成了漏洞背景、原理拆解、复现验证、修复加固和问题排查五大部分阅读时你可以根据自己的角色挑选重点开发同学重点看第 4 部分安全测试同学重点看第 2、3 部分运维同学建议把第 4 和第 5 部分一起看完。1. 漏洞背景与影响面分析1.1 receiptDetail 接口在采购业务中的位置SRM 系统的全称是 Supplier Relationship Management也就是供应商关系管理。它承载着采购方和供应商之间的主要协作流程寻源、合同、订单、发货、收货、对账几乎每个环节都在产生数据。而收货环节里最核心的数据载体就是收货单和它的明细行。receiptDetail 这个接口做的就是按收货单 ID 查询对应的明细数据返回商品编码、数量、含税单价、供应商批次信息、库位等字段。从接口命名就能看出detail 意味着它深入到业务的最细粒度。只要是涉及收货单的页面几乎都会调用这个接口收货列表页要看明细摘要、详情页要展示完整字段、导出报表时需要做分页查询、对账流程中还要按供应商维度汇总数据。可以说它是整个收货流程的数据中枢。这个接口的数据价值非常高。价格、数量、供应商主体信息、付款参考数据都在明细里攻击者一旦拿到查询能力实际上就掌握了采购业务的核心数据面。更麻烦的是很多早期建设的采购系统接口没有严格区分内网和供应商门户的调用方式。同一个查询逻辑既服务于内部管理端也可能暴露在供应商可访问的门户接口中。这意味着风险半径不只限于企业内部账号还可能延伸到整个供应商生态。1.2 为什么这类老采购系统总是容易中招我在评估过的多个采购类系统中发现SQL 注入在存量业务系统里的出现频率远比想象中高。这类系统普遍存在几个共性第一是历史代码包袱重。部分核心查询代码可能是多年前开发的当时团队的开发规范还不严格为了快速上线SQL 语句采用字符串拼接的方式执行参数直接混进 SQL 文本里。后来系统迭代多次但基础查询逻辑很少有人重构隐患就这样被保留下来。第二是 ORM 使用不规范。很多项目用了持久层框架比如 MyBatis但团队对${}和#{}的差异理解不透彻。复习一下#{}在预编译阶段会生成占位符由 PreparedStatement 传值参数永远不会改变 SQL 结构而${}是直接把值拼进 SQL 文本如果值是外部可控的等于把 SQL 的一部分控制权交给了调用者。一个参数用了${}整个接口的基本防线就没了。第三是内网安全假设失效。早期系统部署在隔离网络默认只有内网人员能访问安全问题不明显。但后来系统逐渐迁移到云环境或者通过 API 网关对外提供接口暴露面扩大了很多倍而原有的查询逻辑并没有同步收紧。第四是接口数量多、测试覆盖不足。采购系统动辄几十个查询接口每个接口都有若干参数。很多团队在上线时只验证功能是否正常很少针对非法输入设计测试用例。数据库报错、响应异常、响应时间异常这些信号在真实环境里往往被当作普通故障忽略了。这些因素叠加起来使 receiptDetail 这类高频查询接口成了高危漏洞的高发地。理解这些背景再去看后面的漏洞成因就会清楚很多。2. 漏洞成因与 SQL 注入原理解析2.1 从 HTTP 参数到 SQL 执行一条危险拼接链路receiptDetail 这类查询接口典型的请求链路是HTTP 请求到达控制器控制器解析 URL 中的参数传递给 Service 层Service 层封装查询条件后由持久层框架执行 SQL。问题往往发生在最后一步也就是 SQL 语句生成阶段。曾经有问题的代码简化以后大概是这样的GetMapping(/api/receipt/detail) public ListMapString, Object queryDetail(RequestParam(id) String id, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { return receiptDetailMapper.queryList(id, page, size); }对应的 MyBatis 映射文件里如果是这样写的select idqueryList resultTypemap SELECT * FROM receipt_detail WHERE id ${id} ORDER BY create_time DESC LIMIT #{offset}, #{size} /select这段 XML 里id参数使用了${id}。当请求参数id传入一个普通值1001时SQL 会正常执行。但如果id被传入巧妙的输入SQL 语句就变成了完全不同的样子。原因很简单${id}在做字符串替换时不会对参数做任何类型处理和转义外部的所有字符都会被原样放进 SQL 文本。我在评审代码时有一个很直观的类比方式。#{}就像面试官把问题念给候选人听候选人的任何回答都只是内容不会被当成新的问题继续执行而${}则是把候选人的回答直接补充进面试流程的下一个环节如果回答是精心构造的指令整个流程就会偏离正常轨道。2.2 闭合、注释与条件改变注入的底层逻辑SQL 注入能成立核心在于字符串拼接破坏了 SQL 原本的语法结构。为了方便理解我用一个简化说明当代码里这样写时WHERE id ${id}如果用户传入1001 AND 11拼出来的 SQL 会变成WHERE id 1001 AND 11这个 SQL 依然能正常执行但语义已经被改变。第一个条件是id 1001恒真的附加条件是11。由于第二个条件永远为真查询结果实际上不再严格受 id 限制。如果把恒真条件替换成恒假条件例如1001 AND 12拼出来的 SQL 会变成WHERE id 1001 AND 12由于12永远为假查询结果会是空集。相比正常请求响应结果出现明显差异。这种差异就是安全测试中常说的“布尔条件判断”。它说明参数不仅进入了 SQL还直接参与了查询条件的逻辑运算。除了改变条件还可以通过注释符把后面的 SQL 内容截断掉。在数据库语法里注释符可以把后续文本变成注释从而绕过原本附加在 SQL 末尾的查询条件。这个技巧有时会让原本限制数据范围的 SQL 条件失效加深危害。不过这里并不展开具体绕过手法核心是想说明一个原理只要输入能改变 SQL 结构查询边界就不再受开发者控制。2.3 这类漏洞为什么危险很多非安全背景的读者会想就算能改变查询条件也查不到更多数据吧实际情况并非如此。这类查询接口背后的表往往不只是收货明细本身还可能通过关联查询带出供应商主体信息、采购订单信息、结算数据等。SQL 注入一旦成立攻击者可以逐步扩大查询范围甚至结合数据库自身的报文特性获取更多内容。真正让它危险的原因有三点一是数据敏感。收货明细里包含价格、数量、供应商账户信息这些数据一旦泄露可能被用于商业情报收集甚至进一步诈骗。二是接口可达性高。receiptDetail 是高频接口且可能出现在供应商门户、开放 API、移动端接口等多个入口。攻击者不一定需要攻破后台系统只要找到了一个带有参数的查询入口就可以尝试探测。三是数据库权限往往过大。很多老系统的数据库账号是应用账号直连权限大多很大甚至具备写文件、调用系统命令等高危权限。SQL 注入一旦配合这类权限就不再只是数据查询问题可能演变为服务器层面的严重风险。3. 本地靶场复现与 POC 验证思路3.1 搭一个能放心验证的隔离环境复现这类漏洞前提是获得合法授权并且在隔离的本地环境中进行。我不建议任何人拿线上系统直接试验这一点必须放在最前面说。我当时的做法是在一台本地虚拟机里用 Docker 搭建了一套最小复现环境只保留和漏洞相关的基础组件。环境清单大致如下组件选型说明应用框架Spring Boot 2.x MyBatis模拟真实查询接口的请求链路数据库MySQL 8.0用来观察 SQL 执行行为和报错信息样例数据100 条左右模拟收货单明细保证查询有正常、空集、报错三种响应场景测试工具浏览器 DevTools Python 脚本手工验证探针行为和后续自动化回归数据库里准备了模拟数据每张明细都包含收货单 ID、物料编码、数量、单价、供应商编码等字段。这里需要注意靶场数据不要直接用真实脱敏数据最好完全随机生成避免合规风险。环境启动后第一步是发起正常查询请求确认接口的基础功能和响应结构。比如请求GET /api/receipt/detail?id1001page1size10正常响应会返回一条或多条明细记录。记录下响应体的长度和业务字段后续所有验证都会拿这一步的结果做参照。3.2 注入点定位从一口“报错”说起我定位注入点的过程可以分为四步每一步都非常简单但每一步都有明确的判断目标。第一步请求正常参数。正常获取 id1001 的响应确认接口行为是稳定查询。第二步在参数末尾追加一个单引号。如果原来的查询条件写法是WHERE id ${id}那么单引号会让 SQL 的字符串闭合出现错乱数据库往往直接报语法错误。响应中可能会出现 500 状态码错误页面里可能直接暴露原始 SQL 或数据库异常信息。这一步的意义是确认参数是否被原样拼入了 SQL。第三步字符型布尔验证。把 id 换成1001 AND 11如果返回值与正常请求一致说明 SQL 中的条件被成功吸收额外条件恒真且没有破坏整体语句。第四步恒假条件验证。把 id 换成1001 AND 12如果返回值变成空集或返回记录数明显变化说明参数已经参与逻辑判断。到这一步基本可以判断该参数存在可被利用的 SQL 注入风险。需要说明的是这里展示的探针只是用于确认风险是否存在并不涉及任何数据获取行为。全部验证都在本地靶场完成生产系统和未授权目标一律不允许做类似操作。3.3 设计一个轻量的 POC 脚本确认注入点存在后我写了一个最小化的验证脚本用途不是攻击而是固化验证逻辑方便后续回归时反复使用。脚本的思路非常简单对同一参数同时发起正常请求、恒真条件请求、恒假条件请求对比三组响应的长度和状态码就能自动判断是否存在字符型注入风险。以下脚本只适合在本地授权靶场中使用 本地靶场 SQL 注入布尔探针脚本 仅用于授权环境下的漏洞确认和安全回归测试严禁对未授权系统使用 import requests BASE_URL http://127.0.0.1:8080/api/receipt/detail TARGET_ID 1001 def build_params(pid): normal {id: pid, page: 1, size: 10} # 恒真条件用于确认请求是否能被正常吸收 true_cond {id: f{pid} AND 11, page: 1, size: 10} # 恒假条件用于确认是否参与查询逻辑 false_cond {id: f{pid} AND 12, page: 1, size: 10} return normal, true_cond, false_cond def probe(pid): normal, true_cond, false_cond build_params(pid) results [] for params in (normal, true_cond, false_cond): try: resp requests.get(BASE_URL, paramsparams, timeout5) results.append((resp.status_code, len(resp.text))) except Exception as exc: results.append((0, str(exc))) return results if __name__ __main__: ret probe(TARGET_ID) normal_len ret[0][1] true_len ret[1][1] false_len ret[2][1] print(fnormal: {ret[0]}) print(ftrue: {ret[1]}) print(ffalse: {ret[2]}) # 若恒真与正常响应存在一致特征而恒假响应明显变短或变空则说明条件已生效 if false_len normal_len: print(疑似存在字符型 SQL 注入风险请结合人工判断) else: print(未发现明显差异请检查是否存在其它拼接点)脚本本身很短但设计上考虑了三点一是所有请求都带超时防止资源占用二是只比较响应长度不做数据获取三是输出结果不依赖具体业务字段可以快速接入后续的自动化回归流程。在实际复现过程中我还建议把三组请求的完整响应保存下来尤其是真实请求和恒假请求的差异。如果恒假请求返回空列表或者条目明显减少说明条件已经生效直接定位问题的证据就有了。4. 修复方案与纵深防御实践4.1 根因修复从 ${} 到 #{} 不只是改一个符号修复 SQL 注入最彻底的方法永远是消除 SQL 字符串拼接使用参数化查询。对 MyBatis 来说就是把${}替换成#{}。把前面那段有问题的 XML 改掉后应当是select idqueryList resultTypemap SELECT * FROM receipt_detail WHERE id #{id} ORDER BY create_time DESC LIMIT #{offset}, #{size} /select注意这里有两个细节第一#{}生成的是预编译占位符参数会在执行阶段由驱动安全传值不会改变 SQL 结构。JDBC 层面会自动处理转义单引号、反斜杠等特殊字符都只是字符串内容不会再被当作 SQL 语法元素。第二改造过程中容易踩坑的是动态条件。比如常见查询里按订单号、供应商编码、状态等字段做动态拼接如果改造不完整改造后的接口依然会有遗漏的拼接点。最稳妥的做法是把查询语句里所有参数都过一遍凡是可能被外部输入的一律使用#{}占位。另外LIKE 模糊查询的场景要注意写法。比如查询收货备注信息select idqueryByKeyword resultTypemap SELECT * FROM receipt_detail WHERE remark LIKE CONCAT(%, #{keyword}, %) /select不要把参数直接拼进%${keyword}%。使用CONCAT函数把模糊查询的特殊字符和参数分开传参既安全又保持查询语义。动态排序字段是另一个常见遗漏点。比如ORDER BY ${sortField}排序字段名无法使用占位符需要通过白名单方式解决。正确做法是String sortField create_time; // 默认值 if (update_time.equals(param.getSortField())) { sortField update_time; } else if (quantity.equals(param.getSortField())) { sortField quantity; }只有白名单里明确的字段名才允许进入 SQL 文本。外部传入的任何其他字段名一律使用默认值。4.2 接口层、数据层、配置层的组合拳根因修复虽然解决了注入本身但为了确保系统整体安全还需要做几层加固。这里有一个观念上的变化安全不是只修一个漏洞而是要让同类问题在后续迭代中尽量少出现。接口层要做的事有三件一是参数校验。请求参数进入控制器后第一时间做类型和取值校验。id 应当是数字类型receiptType 应当在系统预定义的枚举范围内。不符合格式的直接返回 400不再进入业务逻辑。二是数据权限校验。收货明细查询不能只依赖 SQL 条件。Service 层需要根据当前登录用户所属供应商或采购组织把可查询的范围限制住。这部分逻辑放在业务代码里比 SQL 条件更可控。三是返回字段最小化。接口只返回前端必要的字段不要把整行数据的所有字段都附带返回。减少数据暴露面可以降低单次接口被异常调用时的损失。数据层的加固重点在数据库账号权限。业务应用对应的数据库账号原则上只保留查询和必要写入权限。报表查询类账号可以单独建立只读账号避免应用被注入时拥有过高权限。数据库账号也不要启用文件读写、命令执行等高级权限这类权限在实际业务中大多用不到。配置层的加固主要是网关和 WAF。在系统入口处加上基础注入特征拦截规则比如包含明显恒真条件、数据库注释符、报错函数特征的请求直接拒绝。但这里必须说一句WAF 是兜底手段不是根本解决方案。它能拦截掉一部分已知特征却很难完全挡住精心构造的绕行语法。真正能兜住底的是代码里不再出现拼接点。4.3 修复后的验证与回归修复完成后需要重新走一遍验证流程。我习惯用一张简单的对照表来梳理测试场景构造输入预期结果正常路径id1001page1size10正常返回明细数据恒真条件id1001 AND 11返回正常数据与恒真条件无关恒假条件id1001 AND 12不因为恒假条件而返回空集非法类型参数idabc返回参数校验错误排序字段异常sortFieldid; DROP TABLE不生效回退默认排序验证时不要只看 HTTP 状态码和前端表现一定要打开数据库日志。观察数据库实际收到的 SQL 语句。如果日志中出现id 1001这样带引号的值说明参数是通过占位符传入的如果出现拼接后的完整 SQL说明链路上还有残留拼接点需要继续排查。回归验证还要覆盖原有业务场景。改动 SQL 后正常查询、分页、排序、导出等原有功能需要重新执行一遍防止因为参数化改造破坏业务逻辑。尤其是分页接口如果查询条件发生变化分页总数的返回可能会不符合预期需要仔细核对。把上面提到的验证用例固化下来放到接口测试用例集里。以后每次发布新版本只要运行一遍就能在最短时间内发现 SQL 拼接是否回归。5. 常见问题与排查技巧实录5.1 复现时响应不符合预期问题出在哪复现过程看起来简单实际操作中很容易碰壁。最典型的几个问题第一参数编码问题。直接通过浏览器地址栏输入 AND 11时部分字符可能被浏览器自动编码或者被中间件截断。尤其是#符号URL 定位符与其后面的文本通常不会被发送到服务器因此注释类字符必须做 URL 编码否则测试结果就不真实。第二请求方法不一致。有的查询接口只接收 POST 请求参数放在请求体里用 GET 方式的探针自然毫无反应。正确的做法是先抓取真实前端请求确认请求方法和请求体格式再在同样格式下构造探针。第三数据库错误信息被屏蔽。有些系统的全局异常处理器把数据库报错统一转换成通用提示即使探针触发了语法错误响应里也看不到任何 SQL 痕迹。这时不能只依赖报错信号还要结合布尔条件响应差异来判断。我的排查思路很简单先把真实请求完整抓包确认参数位置和编码格式再在靶场环境里调整探针格式一项项排查直到能稳定复现。5.2 修完仍然被扫描器报漏洞怎么排查这是很多团队修复后最头疼的问题。代码已经改成了#{}但安全扫描报告依然告警。我遇到过的原因主要有三类。第一类接口内部存在多个拼接点只改了一处。一个接口可能同时支持多个查询参数改造时只处理了注入明显的字段其他字段仍是${}。扫描器发往不同参数立即又能测出问题。排查方法是把接口所有参数都过一遍逐一确认。第二类动态 SQL 的视图或存储过程存在拼接。如果查询走的是数据库视图、存储过程而视图或存储过程内部使用了动态执行语句即使应用层已改掉底层仍可能在执行过程中拼接。这种情况需要把数据库端对象和日志结合起来排查。第三类其他代码路径调用同一个 Mapper。Mapper 方法可能被多个控制器使用修复时只关注了其中一个入口但另一个入口依然传入未经过滤的字符串。建议全局搜索该 Mapper 方法的所有调用位置逐一确认。排查技巧还是回到日志。修复后重新复现一次把数据库端实际执行的 SQL 全部打印出来逐条核对。既然扫描器能测出来就说明某处一定存在拼接通过日志可以快速锁定。5.3 这类漏洞的优先级判断修复资源有限时不能所有接口平均用力。判断优先级可以从三个维度出发。一是数据敏感度。receiptDetail 这类接口返回价格、数量、供应商信息敏感度很高应当优先修复。如果接口还关联出账务、合同等更高敏感字段优先级再往上提。二是暴露面大小。接口是否被供应商门户、移动端、开放 API 等多个入口调用是否不需要复杂权限就能访问。暴露面越大修复优先级越高。三是利用难度。攻击者利用该接口获取数据是否需要满足额外条件比如是否依赖特定账号、是否需要绕过网关白名单。如果利用条件很低说明风险更容易被触发应当放在更前面处理。结合这三个维度采购系统中的高频查询接口通常处于优先修复的位置。修复完成之后也别松劲把验证用例和回归流程沉淀下来让这类问题在团队中形成固定检查项。最后分享一个我自己的习惯。修复完注入类漏洞之后我不会删掉验证用的探针脚本而是会把它改造为最小化的回归测试用例放进日常安全测试中。每次业务迭代跑一遍只要有人不小心把参数重新拼进 SQL它会立刻发出信号。SQL 注入不一定是技术难度最高的漏洞但它对业务数据的威胁非常直接尤其是采购系统这种贴着价格、合同、供应商资金信息走的业务一次注入引发的风险往往不会只停留在单个接口而是可能波及多个关联系统。在开发阶段多用十分钟做预编译比上线后再去追生产事故要划算得多。
延伸阅读

更多相关文章

2026/10/21 23:26:46

个人技能体系构建指南:技能树、优先级排序与刻意练习

1. 技能体系:把自己当成一个产品来打磨 1.1 为什么“skills”值得单独拿出来思考 我遇到过不少这样的人:明明很努力,今天学 Python、明天考 CPA、后天练口才,忙得像陀螺,结果三年过去,简历上能写的东西还是…

2026/10/21 23:26:46

支撑集:稀疏恢复算法的核心与实战指南

支撑集(support),这个词我第一次觉得它重要,不是在上学啃教材的时候,而是在一个稀疏通断恢复项目里被它折腾了几个通宵之后。当时我手里的任务是:从一段严重欠采样的观测里,把原始信号里那些“极…

2026/10/21 23:26:46

数据治理考核绩效建设方案:从PPT到KPI落地的实操指南

简介:本资源是一份面向企业数据治理负责人、IT管理者及数字化转型实践者的《数据治理考核绩效管理建设方案》PPT课件,系统解决数据治理成效难量化、考核与绩效脱节、员工能力提升路径不清晰等落地难题。文件为单个1.33MB的PPTX格式演示文稿,结…

2026/10/22 0:01:49

rea项目极简架构实战:响应式与实时系统的设计、实现与避坑指南

1. 从“rea”这个标题说起:一个被低估的通用缩写第一次看到“rea”这个标题的时候,我脑子里蹦出来的第一反应是——这大概率又是一个被缩写玩坏的项目名。在技术圈混久了你会发现,越是短到只有三个字母的标题,背后藏的东西往往越不…

2026/10/22 0:01:49

大数据人才需求分析毕设实践:从招聘数据到可视化系统

拿到《基于大数据专业岗位招聘信息的人才需求特征分析》这个毕设题目的时候,我第一反应是:终于有个既贴热点、又好落地的方向了。但等我认真帮人搭完一遍完整流程后,发现这个题目远没有看上去那么简单——很多人把它做成了“爬了几千条招聘数…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/21 23:56:48

从零搭建可复用能力体系:skills设计、编排与避坑指南

1. 从“skills”这个词说起:它为什么值得单独拎出来聊“skills”这个词,放在任何一个行业里都不算新鲜。但如果你最近在技术社区、产品圈或者职场分享里频繁看到它被单独拿出来讨论,那大概率不是指泛泛的“技能”概念,而是指向一个…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑