Tai-e: Mybatis完整解决方案
字数 1654 2025-08-03 16:44:05

Tai-e: Mybatis 完整解决方案教学文档

0x00 前言

本教学文档详细介绍了如何使用 Tai-e 静态分析框架扫描 Mybatis 业务代码的完整解决方案。Tai-e 提供了强大的指针分析框架,但针对 Mybatis 的特殊需求需要进行大量二次开发工作。

0x01 处理 Mapper XML 文件获取 Sink

1.1 获取 jar 包/classpath 中的所有 Mapper 文件

World.get().getOptions().getAppClassPath().forEach(appClassPath -> {
    try {
        if (appClassPath.endsWith(".jar")) {
            JarFile jarFile = new JarFile(appClassPath);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.getName().endsWith("Mapper.xml") && !entry.isDirectory()) {
                    InputStream inputStream = jarFile.getInputStream(entry);
                    sinks.addAll(parseXml(inputStream));
                }
            }
        } else {
            try (Stream<Path> paths = Files.walk(Paths.get(appClassPath))) {
                paths.filter(path -> Files.isRegularFile(path) && path.toString().endsWith("Mapper.xml"))
                    .forEach(path -> {
                        try {
                            InputStream inputStream = new FileInputStream(path.toFile());
                            sinks.addAll(parseXml(inputStream));
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                    });
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
});

1.2 解析 Mapper 文件

1.2.1 Mapper XML 执行 SQL 语句的标签总结

常用顶级元素

  • insert - 映射插入语句
  • update - 映射更新语句
  • delete - 映射删除语句
  • select - 映射查询语句

动态拼接语句

  • where - 动态管理 where 子句
  • set - 动态管理 set 子句
  • trim - 控制条件部分两端是否包含某些字符
  • if - 动态判断
  • choose/when/otherwise - 类似 switch 语句
  • foreach - 遍历集合
  • include - 引入其他 SQL 片段
  • sql - 用于抽取重复出现的 SQL 片段

1.2.2 处理上述标签,提取完整 SQL 语句

public static List<MybatisSink> parseXml(InputStream inputStream) {
    List<MybatisSink> sinks = new ArrayList<>();
    try {
        Document document = new SAXReader().read(inputStream);
        Element root = document.getRootElement();
        Attribute namespaceAttr = root.attribute("namespace");
        if (namespaceAttr != null) {
            String mapperNamespace = namespaceAttr.getText();
            JClass jclass = World.get().getClassHierarchy().getClass(mapperNamespace);
            if (jclass != null) {
                for (Iterator<Element> it = root.elementIterator(); it.hasNext(); ) {
                    Element element = it.next();
                    if (element.getName().matches("(insert|update|delete|select)")) {
                        String methodName = element.attribute("id").getText();
                        JMethod jMethod = jclass.getDeclaredMethod(methodName);
                        if (jMethod != null) {
                            final StringBuilder sql = new StringBuilder();
                            sql.append(element.getText().strip());
                            dealInclude(root, element, sql);
                            // 处理子标签...
                            sinks.addAll(isSqli(sql.toString(), jMethod));
                        }
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return sinks;
}

private static void dealInclude(Element root, Element element, StringBuilder sql) {
    element.elements("include").forEach(includeEle -> {
        String refid = includeEle.attribute("refid").getText();
        root.elements("sql").stream()
            .filter(sqlEle -> sqlEle.attribute("id").getText().equals(refid))
            .forEach(sqlEle -> sql.append(sqlEle.getText().strip()));
    });
}

1.3 根据提取的 SQL 语句,确定 sink 参数

1.3.1 Mybatis 传参方式支持的参数类型

简单类型

  • 基本数据类型:int、byte、short、double 等
  • 基本数据类型的包装类型:Integer、Character、Double 等
  • 字符串类型:String

复杂类型

  • 实体类类型:Employee、Department 等
  • 集合类型:List、Set、Map 等
  • 数组类型:int[]、String[] 等
  • 复合类型:List、实体类中包含集合等

1.3.2 不同参数类型的传参方式

  1. 单个简单类型传参方式

    • 通过@Param 指定参数名,在 mapper 中通过#{xxx}的方式接收参数值
    • 不指定参数名,mapper 中通过${value}的方式接收参数值
  2. 实体类类型传参方式

    • Mybatis 会根据${}中传入的数据,加工成 getXxx()方法,然后反射调用实体类对象中的这个方法
  3. 多个简单类型传参方式

    • 通过@Param 指定参数名,在 mapper 中通过${xxx}的方式接收参数值
    • 不指定参数名,mapper 中通过\({param1}或\){参数名}的方式接收参数值
  4. Map 类型传参方式

    • 通过在${}中写 Map 中的 key 来接收参数值
  5. List 类型传参方式

    <foreach collection="empList" item="emp" separator="," open="values" index="myIndex">
      (#{emp.empName},#{myIndex},#{emp.empSalary},#{emp.empGender})
    </foreach>
    

1.3.3 根据不同参数类型,确定 sink 参数

private static Pattern concatPattern = Pattern.compile("\\$\\{(\\w+|\\w+\\.\\w+)\\}");
private static Pattern paramPattern = Pattern.compile("^param(\\d+)$");

public static List<MybatisSink> isSqli(String sql, JMethod method) {
    Set<String> concatArgNames = Sets.newSet();
    List<MybatisSink> sinks = new ArrayList<>();

    Matcher matcher = concatPattern.matcher(sql);
    while(matcher.find()) {
        concatArgNames.add(matcher.group(1));
    }
    
    AtomicBoolean flag = new AtomicBoolean(false);
    concatArgNames.forEach(concatArgName -> {
        if (method.getParamCount() == 1) {
            Type paramType = method.getParamType(0);
            if (WebEntryParamProvider.isJavaBean(paramType)) {
                JField field = World.get().getClassHierarchy().getClass(paramType.getName())
                    .getDeclaredField(concatArgName);
                if (field != null && WebEntryParamProvider.isNotPrimitiveType(field.getType())) {
                    sinks.add(new MybatisSink(method, 0, concatArgName));
                }
            } else {
                flag.set(true);
            }
        } else {
            // 处理多参数情况...
        }
    });
    
    if (flag.get()) {
        sinks.clear();
        // 当方法未使用@Param注解传值时,把所有参数都设置为sink
        for (int i = 0; i < method.getParamCount(); i++) {
            Type paramType = method.getParamType(i);
            if (WebEntryParamProvider.isNotPrimitiveType(paramType)) {
                sinks.add(new MybatisSink(method, i, null));
            }
        }
    }
    return sinks;
}

1.4 将 mybatisSink 添加至污点分析的 sink 中

@Override
public TaintConfig deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
    ObjectCodec oc = p.getCodec();
    JsonNode node = oc.readTree(p);
    List<Source> sources = deserializeSources(node.get("sources"));
    List<Sink> sinks = deserializeSinks(node.get("sinks"));
    List<MybatisSink> mybatisSinks = MybatisHelper.dealMybatisXml(); // 处理mybatis的xml文件
    List<TaintTransfer> transfers = deserializeTransfers(node.get("transfers"));
    List<ParamSanitizer> sanitizers = deserializeSanitizers(node.get("sanitizers"));
    JsonNode callSiteNode = node.get("call-site-mode");
    TransInferConfig inferenceConfig = deserializeInferenceConfig((node.get("transfer-inference")));
    boolean callSiteMode = (callSiteNode != null && callSiteNode.asBoolean());
    return new TaintConfig(
            sources, sinks, mybatisSinks, transfers, sanitizers, callSiteMode, inferenceConfig);
}

0x02 处理 Mybatis 注解获取 Sink

2.1 解决 Mybatis 业务方法是抽象方法的问题

  1. 为 Mybatis 业务对象创建虚拟对象:
if(componentSubs.size() == 1 && componentSubs.get(0).hasAnnotation(ComponentType.MapperType.getName())) {
    return heapModel.getMockObj(() -> "DependencyInjectionMapperObj", 
            field.getRef(), componentSubs.get(0).getType());
}
  1. 处理抽象函数调用:
private void processCall(CSVar recv, PointsToSet pts) {
    Context context = recv.getContext();
    Var var = recv.getVar();
    for (Invoke callSite : var.getInvokes()) {
        pts.forEach(recvObj -> {
            JMethod callee = CallGraphs.resolveCallee(recvObj.getObject().getType(), callSite);
            // 处理Mybatis的接口函数调用
            if (callee == null && recvObj.getObject() instanceof MockObj mockObj 
                    && mockObj.getDescriptor().string().equals("DependencyInjectionMapperObj")) {
                plugin.onCallMybatisMethod(recvObj, callSite);
                callee = callSite.getMethodRef().resolve();
            }
            // 其他处理...
        });
    }
}
  1. 为抽象函数构造空函数体:
public IR getIR() {
    if (ir == null) {
        if (isAbstract()) {
            return new IRBuildHelper(this).buildEmpty();
        }
        // 其他处理...
    }
    return ir;
}
  1. 处理 Mybatis 注解:
@Override
public void onCallMybatisMethod(CSObj recv, Invoke invoke) {
    if (recv.getObject() instanceof MockObj mockObj 
            && mockObj.getDescriptor().string().equals("DependencyInjectionMapperObj")) {
        JMethod method = invoke.getMethodRef().resolve();
        method.getAnnotations().stream()
            .filter(annotation -> annotation.getType().matches(
                "org\\.apache\\.ibatis\\.annotations\\.(Select|Delete|Insert|Update)") 
                && annotation.hasElement("value"))
            .forEach(annotation -> {
                String sql = annotation.getElement("value").toString();
                mybatisSinks.addAll(MybatisHelper.isSqli(sql, method));
            });
    }
}

0x03 Mybatis 污点 Sink 匹配

mybatisSinks.forEach(sink -> {
    int i = sink.index();
    String f = sink.field();
    result.getCallGraph()
            .edgesInTo(sink.method())
            .filter(e -> e.getKind() != CallKind.OTHER)
            .map(Edge::getCallSite)
            .forEach(sinkCall -> {
                Var arg = InvokeUtils.getVar(sinkCall, i);
                SinkPoint sinkPoint = new SinkPoint(sinkCall, i);
                result.getPointsToSet(arg)
                        .stream()
                        .map(obj -> {
                            Type objType = obj.getType();
                            if (f != null) {
                                JField jField = World.get().getClassHierarchy()
                                    .getClass(objType.getName()).getDeclaredField(f);
                                if (jField != null) {
                                    for (Obj fieldObj: result.getPointsToSet(obj, jField)) {
                                        if (manager.isTaint(fieldObj)) {
                                            return fieldObj;
                                        }
                                    }
                                }
                            }
                            return obj;
                        })
                        .filter(manager::isTaint)
                        .map(manager::getSourcePoint)
                        .map(sourcePoint -> new TaintFlow(sourcePoint, sinkPoint))
                        .forEach(taintFlows::add);
            });
});

0x04 Mybatis 污点传播问题处理

4.1 为 SpringMVC 入口函数的 JavaBean 对象的字段创建污点对象

@Override
public void onNewPointsToSet(CSVar csVar, PointsToSet pts) {
    pts.forEach(baseObj -> {
        if (webEntryParamObj.add(baseObj) && baseObj.getObject() instanceof MockObj mockObj 
                && mockObj.getDescriptor().string().equals("WebEntryParamObj")) {
            Type type = baseObj.getObject().getType();
            if (type instanceof ClassType cType) {
                for (JField field : cType.getJClass().getDeclaredFields()) {
                    Type fieldType = field.getType();
                    if (WebEntryParamProvider.isInstantiable(fieldType) 
                            && WebEntryParamProvider.isNotPrimitiveType(fieldType)) {
                        JMethod fieldSetter = WebEntryParamProvider.getFieldSetter(cType, field);
                        if (fieldSetter != null) {
                            SourcePoint sourcePoint = new ParamSourcePoint(fieldSetter, 0);
                            Obj taint = manager.makeTaint(sourcePoint, fieldType);
                            InstanceField iField = solver.getCSManager()
                                .getInstanceField(baseObj, field);
                            solver.addPointsTo(iField, 
                                solver.getContextSelector().getEmptyContext(), taint);
                        }
                    }
                }
            }
        }
    });
}

4.2 Mybatis Sink 参数为 Map 类型时的污点传播

在污点分析配置文件中添加:

transfers:
  - {
      method: '<java.util.HashMap: java.lang.Object put(java.lang.Object,java.lang.Object)>',
      from: 1,
      to: base,
    }

4.3 Mybatis Sink 参数为 List 类型时的污点传播

  1. 基础配置:
transfers:
  - {
      method: '<java.util.ArrayList: boolean add(java.lang.Object)>',
      from: 0,
      to: base,
    }
  1. 处理实体对象中的字段传播:
@Override
public void onNewCallEdge(Edge<CSCallSite, CSMethod> edge) {
    if (edge.getKind() == CallKind.OTHER) {
        return;
    }
    JMethod method = edge.getCallee().getMethod();
    method2CSCallSite.put(method, edge.getCallSite());
    Set<TaintTransfer> tfs = transfers.get(method);
    if (!tfs.isEmpty()) {
        Context context = edge.getCallSite().getContext();
        Invoke callSite = edge.getCallSite().getCallSite();
        tfs.forEach(tf -> processTransfer(context, callSite, tf));
    }
    // 将set方法中的污点对象传播给调用该set方法的对象
    if (method.getName().startsWith("set") 
            && WebEntryParamProvider.isNotPrimitiveType(method.getParamType(0)) 
            && WebEntryParamProvider.isJavaBean(method.getDeclaringClass().getType())) {
        TaintTransfer tf = new ConcreteTransfer(method, 
            new TransferPoint(TransferPoint.Kind.VAR, 0, null), 
            new TransferPoint(TransferPoint.Kind.VAR, -1, null), 
            method.getDeclaringClass().getType());
        processTransfer(edge.getCallSite().getContext(), edge.getCallSite().getCallSite(), tf);
    }
}

0x05 总结

本方案完整解决了 Tai-e 扫描 Mybatis 业务代码时遇到的所有问题,包括:

  1. 处理 Mapper XML 文件获取 Sink
  2. 处理 Mybatis 注解获取 Sink
  3. Mybatis 污点 Sink 匹配
  4. Mybatis 污点传播问题处理

完整代码已开源在 GitHub: https://github.com/hldfight/Tai-e-WebPlugin

Tai-e: Mybatis 完整解决方案教学文档 0x00 前言 本教学文档详细介绍了如何使用 Tai-e 静态分析框架扫描 Mybatis 业务代码的完整解决方案。Tai-e 提供了强大的指针分析框架,但针对 Mybatis 的特殊需求需要进行大量二次开发工作。 0x01 处理 Mapper XML 文件获取 Sink 1.1 获取 jar 包/classpath 中的所有 Mapper 文件 1.2 解析 Mapper 文件 1.2.1 Mapper XML 执行 SQL 语句的标签总结 常用顶级元素 : insert - 映射插入语句 update - 映射更新语句 delete - 映射删除语句 select - 映射查询语句 动态拼接语句 : where - 动态管理 where 子句 set - 动态管理 set 子句 trim - 控制条件部分两端是否包含某些字符 if - 动态判断 choose/when/otherwise - 类似 switch 语句 foreach - 遍历集合 include - 引入其他 SQL 片段 sql - 用于抽取重复出现的 SQL 片段 1.2.2 处理上述标签,提取完整 SQL 语句 1.3 根据提取的 SQL 语句,确定 sink 参数 1.3.1 Mybatis 传参方式支持的参数类型 简单类型 : 基本数据类型:int、byte、short、double 等 基本数据类型的包装类型:Integer、Character、Double 等 字符串类型:String 复杂类型 : 实体类类型:Employee、Department 等 集合类型:List、Set、Map 等 数组类型:int[]、String[ ] 等 复合类型:List 、实体类中包含集合等 1.3.2 不同参数类型的传参方式 单个简单类型传参方式 : 通过@Param 指定参数名,在 mapper 中通过#{xxx}的方式接收参数值 不指定参数名,mapper 中通过${value}的方式接收参数值 实体类类型传参方式 : Mybatis 会根据${}中传入的数据,加工成 getXxx()方法,然后反射调用实体类对象中的这个方法 多个简单类型传参方式 : 通过@Param 指定参数名,在 mapper 中通过${xxx}的方式接收参数值 不指定参数名,mapper 中通过${param1}或${参数名}的方式接收参数值 Map 类型传参方式 : 通过在${}中写 Map 中的 key 来接收参数值 List 类型传参方式 : 1.3.3 根据不同参数类型,确定 sink 参数 1.4 将 mybatisSink 添加至污点分析的 sink 中 0x02 处理 Mybatis 注解获取 Sink 2.1 解决 Mybatis 业务方法是抽象方法的问题 为 Mybatis 业务对象创建虚拟对象: 处理抽象函数调用: 为抽象函数构造空函数体: 处理 Mybatis 注解: 0x03 Mybatis 污点 Sink 匹配 0x04 Mybatis 污点传播问题处理 4.1 为 SpringMVC 入口函数的 JavaBean 对象的字段创建污点对象 4.2 Mybatis Sink 参数为 Map 类型时的污点传播 在污点分析配置文件中添加: 4.3 Mybatis Sink 参数为 List 类型时的污点传播 基础配置: 处理实体对象中的字段传播: 0x05 总结 本方案完整解决了 Tai-e 扫描 Mybatis 业务代码时遇到的所有问题,包括: 处理 Mapper XML 文件获取 Sink 处理 Mybatis 注解获取 Sink Mybatis 污点 Sink 匹配 Mybatis 污点传播问题处理 完整代码已开源在 GitHub: https://github.com/hldfight/Tai-e-WebPlugin