ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

23 openclaw防止SQL注入:参数化查询与ORM安全使用

23 openclaw防止SQL注入:参数化查询与ORM安全使用 1. openclaw 订单查询被一条OR 11打穿的真实场景openclaw 是一个典型的 Java 后台项目订单、报表、客户管理这些模块都跑在 MyBatis 之上。SQL 注入能做什么简单说它让攻击者把用户输入当成 SQL 代码执行轻则拖走整张表重则删库。适合谁看正在维护 openclaw 或类似 MyBatis 项目、需要把注入风险堵在代码层的后端同学。去年一次安全审计里我们复现了一个看似无害的订单查询当用户输入订单号12345 OR 11时接口返回了全量订单。根因不是数据库配置而是开发同学在 Mapper XML 里用了${orderId}做字符串替换。MyBatis 的#{}走预编译${}走文本拼接一字之差防线全无。这篇按「先定位问题 → 再上参数化 → 再收口 ORM → 最后自测验证」的顺序拆每一步都给可复制的配置和代码。文中涉及 AI 辅助排查时统一走 TaoToken 的 Key/API 通道避免在多个工具间来回切账号。2. 前置用 TaoToken 统一 Key 与 API 通道辅助排查注入点排查 SQL 注入时我习惯让 AI 工具帮我扫 Mapper XML 里的${}、生成注入用例、解释报错堆栈。问题是不同工具要配不同 Key管理起来很碎。TaoToken 的做法是提供一个统一的 API 入口把模型调用收敛到一套 Key 上。你需要先拿到 Key打开 https://taotoken.net/api-keys 创建然后按接入文档 https://taotoken.net/doc 配置 base_url 和模型名。模型对话入口在 https://taotoken.net/model-chat 适合临时贴一段 Mapper XML 让模型标出风险行如果是长期做代码审计、Agent 类任务用 Coding Plan https://taotoken.net/coding-plan 更省心。注意TaoToken 在这里的角色是「AI 工具的调用通道」不是数据库代理也不参与你的 SQL 执行链路。数据库连接仍然走你自己的连接池。配置示例以 OpenAI 兼容客户端为例export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/apifrom openai import OpenAI client OpenAI( api_keysk-你的Key, base_urlhttps://taotoken.net/api ) resp client.chat.completions.create( modelclaude-sonnet-4-5, messages[ {role: user, content: 下面这段 MyBatis XML 有哪些 SQL 注入风险点只列风险行和修法。\nselect idqSELECT * FROM orders WHERE id${id}/select} ] ) print(resp.choices[0].message.content)跑通后你会拿到类似「第 1 行${id}应改为#{id}」的结论接着就能进入下面的代码改造。3. 可复制配置MyBatis 参数化查询与 ORM 安全骨架3.1 先分清#{}和${}这是 openclaw 里最高频的坑。#{}生成?占位符由 JDBC 预编译参数永远当数据${}直接拼进 SQL 文本参数会被当代码解析。!-- 危险字符串替换可注入 -- select idfindOrder resultTypeOrder SELECT * FROM orders WHERE order_id ${orderId} /select !-- 安全预编译占位符 -- select idfindOrder resultTypeOrder SELECT * FROM orders WHERE order_id #{orderId} /select动态 SQL 里同样要守住这条线!-- 危险 -- if teststatus ! null AND status ${status} /if !-- 安全 -- if teststatus ! null AND status #{status} /if3.2 动态排序、动态列名怎么办ORDER BY后面的列名不能用#{}因为占位符不能当标识符。正确做法是白名单校验而不是退回${}裸拼。private static final SetString ALLOWED_SORT Set.of( order_id, create_time, amount, status ); public String safeSortColumn(String input) { if (input null || !ALLOWED_SORT.contains(input)) { return order_id; // 默认值绝不把原始输入拼进 SQL } return input; }Mapper 里这样用select idlistOrders resultTypeOrder SELECT * FROM orders where if teststatus ! nullAND status #{status}/if if testcustomerId ! nullAND customer_id #{customerId}/if /where ORDER BY ${sortColumn} ${sortDirection} /select注意这里的${sortColumn}和${sortDirection}必须来自上面的白名单方法且sortDirection只允许ASC/DESC两个值。任何直接来自请求参数的${}都是漏洞。3.3 统一参数化查询封装openclaw 里我加了一个SafeQuery工具类所有手写 SQL 都走它杜绝Statement拼接public class SafeQuery { public static ResultSet query(Connection conn, String sql, Object... params) throws SQLException { PreparedStatement pstmt conn.prepareStatement(sql); for (int i 0; i params.length; i) { pstmt.setObject(i 1, params[i]); } return pstmt.executeQuery(); } }3.4 ORM 查询骨架多条件组合查询订单高级查询是注入重灾区用 Builder 把「列名」和「值」分开管理public class SafeQueryBuilder { private final StringBuilder sql new StringBuilder(); private final ListObject params new ArrayList(); public SafeQueryBuilder(String base) { this.sql.append(base); } public SafeQueryBuilder where(String column, Object value) { sql.append( AND ).append(column).append( ?); params.add(value); return this; } public String getSql() { return sql.toString(); } public Object[] getParams() { return params.toArray(); } }调用侧public ListOrder findOrders(OrderQuery q) { SafeQueryBuilder b new SafeQueryBuilder(SELECT * FROM orders WHERE 11); if (q.getOrderId() ! null) b.where(order_id, q.getOrderId()); if (q.getStatus() ! null) b.where(status, q.getStatus()); if (q.getCustomerId() ! null) b.where(customer_id, q.getCustomerId()); try { ResultSet rs SafeQuery.query(conn, b.getSql(), b.getParams()); return convertToOrders(rs); } catch (SQLException e) { throw new RuntimeException(查询订单失败, e); } }关键点where()里的column必须是代码里写死的常量不能来自用户输入value一律走?占位符。3.5 报表动态列查询报表允许用户选列列名同样要白名单public ListMapString, Object generateReport(String[] selectedColumns, MapString, Object filters) { SetString allowed Set.of(order_id, customer_name, amount, status); for (String col : selectedColumns) { if (!allowed.contains(col)) { throw new IllegalArgumentException(非法列名: col); } } String columnList String.join(, , selectedColumns); SafeQueryBuilder b new SafeQueryBuilder( SELECT columnList FROM orders WHERE 11); filters.forEach((k, v) - { if (allowed.contains(k)) b.where(k, v); }); try { ResultSet rs SafeQuery.query(conn, b.getSql(), b.getParams()); return convertToMapList(rs); } catch (SQLException e) { throw new RuntimeException(生成报表失败, e); } }4. 验证请求注入用例与成功结果改完不能靠肉眼要跑用例。下面这组用例覆盖了 openclaw 里最常见的注入入口。用例编号输入预期结果T112345 OR 11返回空或仅匹配字面量不返回全量T212345; DROP TABLE orders;--查询无异常表结构完好T312345 UNION SELECT username,password FROM users不返回 users 数据T4排序参数amount; DELETE FROM orders回退默认排序不执行删除用 curl 打接口验证curl -X GET http://localhost:8080/api/orders?orderId12345%20OR%201%3D1 \ -H Authorization: Bearer token修复前返回全量订单修复后返回空数组或仅12345这一条。再查一次表结构确认没被破坏SHOW TABLES LIKE orders; SELECT COUNT(*) FROM orders;如果 T2 用例执行后orders表还在、数据条数不变说明参数化生效。T3 用例重点看响应体里有没有出现password字段出现即说明 UNION 注入成功必须回查对应 Mapper。5. 本篇常见错排查5.1 改了#{}还是被注入先确认改的是不是真正执行的那条 SQL。openclaw 里存在 Mapper 继承和include复用可能你改的片段没被实际调用。用 MyBatis 日志打出最终 SQLmybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl日志里如果看到Parameters: 12345 OR 11(String)且 SQL 是WHERE order_id ?说明参数化生效如果看到 SQL 里直接嵌了OR 11说明还有${}没清干净。5.2ORDER BY报语法错误把${}改成#{}后ORDER BY ?会报错因为占位符不能当列名。这不是参数化失效而是用错了位置。回到 3.2 的白名单方案列名走校验、值走占位符。5.3 动态表名、动态 schema同理表名不能用#{}。做法是枚举合法表名private static final SetString ALLOWED_TABLES Set.of(orders, orders_2024); if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException(非法表名); }5.4 检测器误报有些团队加了关键字检测见下把正常含select的搜索词也拦了。检测器只能当辅助不能替代参数化。真正的防线是#{}和白名单检测器用于告警而非阻断。public class SqlInjectionDetector { private static final SetString KEYWORDS Set.of( SELECT, INSERT, UPDATE, DELETE, DROP, UNION, EXEC); public static boolean containsSqlInjection(String input) { if (input null) return false; String upper input.toUpperCase(); return KEYWORDS.stream().anyMatch(upper::contains); } }5.5 用 AI 辅助排查时 Key 报 401如果你按第 2 节配置后调用返回 401先确认base_url是https://taotoken.net/apiKey 从 https://taotoken.net/api-keys 复制完整。模型对话场景可以直接在 https://taotoken.net/model-chat 里贴 Mapper XML 验证通道是否通通了再回到本地脚本。6. 把防护固化进 openclaw 的日常流程参数化查询和 ORM 安全使用不是一次性改造而是每次提交都要守的规矩。我的做法是Mapper XML 里出现${}必须走 Code Review 白名单说明新增查询一律用SafeQueryBuilder上线前跑一遍第 4 节的注入用例。如果你也在用 AI 工具做代码审计或生成注入用例建议把 Key 和 API 通道统一到 TaoToken接入文档在 https://taotoken.net/doc 创建 Key 在 https://taotoken.net/api-keys 临时验证模型走 https://taotoken.net/model-chat 长期做编码和 Agent 任务用 https://taotoken.net/coding-plan 。这样排查注入点时不用在多个工具间切账号审计记录也能集中管理。最后留一个我踩过的坑白名单校验一定要在 Service 层做不要只在 Controller 做。openclaw 里有内部调用直接进 Service 的路径绕过 Controller 就等于绕过校验。把校验下沉到最靠近 SQL 的那一层才是真的收口。
返回列表