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 不同参数类型的传参方式
-
单个简单类型传参方式:
- 通过@Param 指定参数名,在 mapper 中通过#{xxx}的方式接收参数值
- 不指定参数名,mapper 中通过${value}的方式接收参数值
-
实体类类型传参方式:
- Mybatis 会根据${}中传入的数据,加工成 getXxx()方法,然后反射调用实体类对象中的这个方法
-
多个简单类型传参方式:
- 通过@Param 指定参数名,在 mapper 中通过${xxx}的方式接收参数值
- 不指定参数名,mapper 中通过\({param1}或\){参数名}的方式接收参数值
-
Map 类型传参方式:
- 通过在${}中写 Map 中的 key 来接收参数值
-
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 业务方法是抽象方法的问题
- 为 Mybatis 业务对象创建虚拟对象:
if(componentSubs.size() == 1 && componentSubs.get(0).hasAnnotation(ComponentType.MapperType.getName())) {
return heapModel.getMockObj(() -> "DependencyInjectionMapperObj",
field.getRef(), componentSubs.get(0).getType());
}
- 处理抽象函数调用:
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();
}
// 其他处理...
});
}
}
- 为抽象函数构造空函数体:
public IR getIR() {
if (ir == null) {
if (isAbstract()) {
return new IRBuildHelper(this).buildEmpty();
}
// 其他处理...
}
return ir;
}
- 处理 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 类型时的污点传播
- 基础配置:
transfers:
- {
method: '<java.util.ArrayList: boolean add(java.lang.Object)>',
from: 0,
to: base,
}
- 处理实体对象中的字段传播:
@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 业务代码时遇到的所有问题,包括:
- 处理 Mapper XML 文件获取 Sink
- 处理 Mybatis 注解获取 Sink
- Mybatis 污点 Sink 匹配
- Mybatis 污点传播问题处理
完整代码已开源在 GitHub: https://github.com/hldfight/Tai-e-WebPlugin