Mybatis OGNL導致的併發安全問題

Mybatis是一個開源的輕量級半自動化ORM框架,使得面向對象應用程序與關係數據庫的映射變得更加容易。MyBatis使用xml描述符或註解將對象與存儲過程或SQL語句相結合。Mybatis最大優點是應用程序與Sql進行解耦,sql語句是寫在Xml Mapper文件中。

OGNL表達式在Mybatis當中應用非常廣泛,其表達式的靈活性使得動態Sql功能的非常強大。OGNL是Object-Graph Navigation Language的縮寫,代表對象圖導航語言。OGNL是一種EL表達式語言,用於設置和獲取Java對象的屬性,並且可以對列表進行投影選擇以及執行lambda表達式。Ognl類提供了許多簡便方法用於執行表達式的。Struts2發佈的每個版本都會出現的新的高危可執行漏洞也是因爲它使用了靈活的OGNL表達式。

公司後端採用Mybatis作爲數據訪問層,所使用版本爲3.2.3。線上環境業務系統在運行過程中出現了一個令人困惑的異常, 該異常時而出現時而不出現,構造各種OGNL表達式爲空等特殊情況均不會重現該異常。具體異常堆棧信息如下:

### Error querying database. Cause: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'list != null and list.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
### Cause: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'list != null and list.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
 at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:23) org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:107)
 at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:98)
 at cn.com.shaobingmm.MybatisBugTest$2.run(MybatisBugTest.java:88)
 at java.lang.Thread.run(Thread.java:745)
Caused by: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'list != null and list.size() > 0'. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
 at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java
 at:47)
 at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:29)
 at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:30)
 at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:29)
 at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:51)
 at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:29)
 at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:37)
 at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:275)
 at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:79)
 at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:104)
 ... 3 more
Caused by: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers "public"]
 at org.apache.ibatis.ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:837)
 at org.apache.ibatis.ognl.ObjectMethodAccessor.callMethod(ObjectMethodAccessor.java:61)
 at org.apache.ibatis.ognl.OgnlRuntime.callMethod(OgnlRuntime.java:860)
 at org.apache.ibatis.ognl.ASTMethod.getValueBody(ASTMethod.java:73)
 at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
 at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
 at org.apache.ibatis.ognl.ASTChain.getValueBody(ASTChain.java:109)
 at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
 at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
 at org.apache.ibatis.ognl.ASTGreater.getValueBody(ASTGreater.java:49)
 at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
 at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
 at org.apache.ibatis.ognl.ASTAnd.getValueBody(ASTAnd.java:56)
 at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170)
 at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210)
 at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:333)
 at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:413)
 at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:395)
 at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:45)
 ... 12 more

List的size()方法明顯是public爲何還會出現不可訪問的異常。該問題並不是每一次

都會出現,經過多次嘗試,該異常一直未在測試環境重現。該接口在完整調用鏈路中

的出錯次數佔總調用次數的比率爲0.01%,無意中聯想到併發問題在週期性時間內往往

是概率性發生。

編寫模擬多線程環境併發讀取公司列表測試代碼:

String resource = "mybatis-config.xml";
 InputStream in = null;
 try {
 in = Resources.getResourceAsStream(resource);
 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
 final List<Long> ids = Collections.singletonList(1L);
 final SqlSession session = sqlSessionFactory.openSession();
 final CountDownLatch mCountDownLatch = new CountDownLatch(1);
 for (int i = 0; i < 50; i++) {
 Thread thread = new Thread(new Runnable() {
 public void run() {
 try {
 mCountDownLatch.await();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 for (int k = 0; k < 100; k++) {
 session.selectList("CompanyMapper.getCompanysByIds", ids);
 }
 }
 });
 thread.start();
 }
 mCountDownLatch.countDown();
 synchronized (MybatisBugTest.class) {
 try {
 MybatisBugTest.class.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 } catch (IOException e) {
 e.printStackTrace();
 } catch (Throwable e) {
 e.printStackTrace();
 } finally {
 if (in != null)
 try {
 in.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
 }

上訴異常堆棧信息在併發環境下果然重現出現,根據異常信息代碼執行至該行代碼時發生異常:

Caused by: org.apache.ibatis.ognl.MethodFailedException: Method “size” failed for object [1] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Collections$SingletonList with modifiers “public”]
at org.apache.ibatis.ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:837)
異常信息表明OgnlRuntime類不能夠訪問java.util.Collections的私有成員

SingletonList。查看源代碼發現能夠拋出MethodFailedException異常可以鎖定

在invokeMethod方法內部。


```handlebars
public static Object callAppropriateMethod(OgnlContext context, Object source, Object target, String methodName, String propertyName, List methods, Object[] args) throws MethodFailedException {
 Object reason = null;
 Object[] actualArgs = objectArrayPool.create(args.length);
 try {
 Method e = getAppropriateMethod(context, source, target, methodName, propertyName, methods, args, actualArgs);
 if(e == null || !isMethodAccessible(context, source, e, propertyName)) {
 StringBuffer buffer = new StringBuffer();
 if(args != null) {
 int i = 0;
 for(int ilast = args.length - 1; i <= ilast; ++i) {
 Object arg = args[i];
 buffer.append(arg == null?NULL_STRING:arg.getClass().getName());
 if(i < ilast) {
 buffer.append(", ");
 }
 }
 }
 throw new NoSuchMethodException(methodName + "(" + buffer + ")");
 }
 Object var14 = invokeMethod(target, e, actualArgs);
 return var14;
 } catch (NoSuchMethodException var21) {
 reason = var21;
 } catch (IllegalAccessException var22) {
 reason = var22;
 } catch (InvocationTargetException var23) {
 reason = var23.getTargetException();
 } finally {
 objectArrayPool.recycle(actualArgs);
 }
 throw new MethodFailedException(source, methodName, (Throwable)reason);
 }

invokeMethod方法代碼

```java
public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
 boolean wasAccessible = true;
 if(securityManager != null) {
 try {
 securityManager.checkPermission(getPermission(method));
 } catch (SecurityException var6) {
 throw new IllegalAccessException("Method [" + method + "] cannot be accessed.");
 }
 }
 if((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !(wasAccessible = method.isAccessible())) {
 method.setAccessible(true); (1)
 }
 Object result = method.invoke(target, argsArray); (3)
 if(!wasAccessible) {
 method.setAccessible(false); (2)
 }
 return result;
 }

問題出現在method實際上是一個共享變量,也就是例子中的

public int java.util.Collections$SingletonList.size()

方法

當第一個線程t1至(1)行代碼允許method方法可以被調用,第二個線程t2執行至(2)將method的方法設置爲不可以訪問。接着t1又開始執行到(3)行的時候就會發生該異常。這是一個很典型的同步問題。

Ognl2.7已經修復了該問題,因爲ognl源碼是直接打包內嵌在mybatis包中,mybatis3.3.0版本中也已經進行了修復升級。(劃重點)

> public static Object invokeMethod(Object target, Method method,
> Object[] argsArray) throws InvocationTargetException,
> IllegalAccessException {  boolean syncInvoke = false;  boolean
> checkPermission = false;  int mHash = method.hashCode(); 
> synchronized(method) { 
> if(_methodAccessCache.get(Integer.valueOf(mHash)) == null ||
> _methodAccessCache.get(Integer.valueOf(mHash)) == Boolean.TRUE) {  syncInvoke = true;  }  if(_securityManager != null &&
> _methodPermCache.get(Integer.valueOf(mHash)) == null || _methodPermCache.get(Integer.valueOf(mHash)) == Boolean.FALSE) {  checkPermission = true;  }  }  boolean wasAccessible = true;  Object
> result;  if(syncInvoke) {  synchronized(method) {  if(checkPermission)
> {  try {  _securityManager.checkPermission(getPermission(method)); 
> _methodPermCache.put(Integer.valueOf(mHash), Boolean.TRUE);  } catch (SecurityException var12) { 
> _methodPermCache.put(Integer.valueOf(mHash), Boolean.FALSE);  throw new IllegalAccessException("Method [" + method + "] cannot be
> accessed.");  }  }  if(Modifier.isPublic(method.getModifiers()) &&
> Modifier.isPublic(method.getDeclaringClass().getModifiers())) { 
> _methodAccessCache.put(Integer.valueOf(mHash), Boolean.FALSE);  } else if(!(wasAccessible = method.isAccessible())) { 
> method.setAccessible(true); 
> _methodAccessCache.put(Integer.valueOf(mHash), Boolean.TRUE);  } else {  _methodAccessCache.put(Integer.valueOf(mHash), Boolean.FALSE);  } 
> result = method.invoke(target, argsArray);  if(!wasAccessible) { 
> method.setAccessible(false);  }  }  } else {  if(checkPermission) { 
> try {  _securityManager.checkPermission(getPermission(method)); 
> _methodPermCache.put(Integer.valueOf(mHash), Boolean.TRUE);  } catch (SecurityException var11) { 
> _methodPermCache.put(Integer.valueOf(mHash), Boolean.FALSE);  throw new IllegalAccessException("Method [" + method + "] cannot be
> accessed.");  }  }  result = method.invoke(target, argsArray);  } 
> return result;  }

免費分享Java學習資料,需要的朋友可以在關乎後私信我

原文:https://zhuanlan.zhihu.com/p/30085658
作者:蓬蒿
來源:知乎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章