├── .gitignore ├── README.md ├── app-sample-screenshot.png ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── doc │ ├── injected.js │ └── notRepeat.js └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── android │ └── webkit │ └── safe │ ├── JsCallJava.java │ ├── JsCallback.java │ └── SafeWebView.java ├── sample ├── build.gradle ├── proguard-android.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── test.html │ └── unsafe_test.html │ ├── java │ └── android │ │ └── webkit │ │ └── safe │ │ └── sample │ │ ├── JavaScriptInterface.java │ │ ├── UnsafeWebActivity.java │ │ └── WebActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-ldpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ └── values │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # built native files 12 | # *.o 13 | # *.so 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | 19 | # Android Studio 20 | .idea 21 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 22 | .gradle 23 | build/ 24 | 25 | */out 26 | */build 27 | */production 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | # Proguard folder generated by Eclipse 33 | proguard/ 34 | 35 | # Log Files 36 | *.log 37 | *.iml 38 | *.iws 39 | *.ipr 40 | 41 | # Windows thumbnail db 42 | Thumbs.db 43 | 44 | # OSX files 45 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SafeWebView 2 | =================== 3 | 解决2个问题: 4 | 5 | 1、WebView addJavascriptInterface安全漏洞问题; 6 | 2、支持网页将JS函数(function)传到Java层,方便回调; 7 | 8 | ![image](app-sample-screenshot.png) 9 | 10 | ## 原理 11 | 使用prompt中转反射调用Java层接口类中的方法,将方法名、参数类型、参数封装成Json进行传递; 12 | 另参照: 13 | 1、在WebView中如何让JS与Java安全地互相调用:http://www.pedant.cn/2014/07/04/webview-js-java-interface-research/,源码(20150621):https://github.com/pedant/safe-java-js-webview-bridge 14 | 2、Android WebView的Js对象注入漏洞解决方案:http://blog.csdn.net/leehong2005/article/details/11808557 15 | 16 | ## 用法 17 | 18 | ### 初始化Webview WebSettings时允许js脚本执行,SafeWebView(自己的WebView可以继承这个类)内部重写了addJavascriptInterface和setWebChromeClient方法: 19 | 20 | WebView wv = new SafeWebView(this); 21 | WebSettings ws = wv.getSettings(); 22 | ws.setJavaScriptEnabled(true); 23 | wv.addJavascriptInterface(new JavaScriptInterface(wv), "Android"); 24 | wv.setWebChromeClient(new InnerChromeClient()); 25 | wv.loadUrl("file:///android_asset/test.html"); 26 | 27 | ### 自定义WebChromeClient子类 28 | 29 | public class InnerChromeClient extends SafeWebChromeClient { 30 | 31 | @Override 32 | public void onProgressChanged (WebView view, int newProgress) { 33 | super.onProgressChanged(view, newProgress); // 务必放在方法体的第一行执行; 34 | // to do your work 35 | // ... 36 | } 37 | 38 | @Override 39 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 40 | // to do your work 41 | // ... 42 | return super.onJsPrompt(view, url, message, defaultValue, result); // 务必放在方法体的最后一行执行,或者用if判断也行; 43 | } 44 | } 45 | 46 | 注意:由于JS中数字类型不区分整型、长整型、浮点类型等,是统一由64位浮点数表示,故Java方法在定义时int/long/double被当作是一种类型double; 47 | 48 | ### 关于异步回调 49 | 举例说明,首先你可以在Java层定义如下方法,该方法的作用是延迟设定的时间之后,用你传入的参数回调Js函数: 50 | 51 | @android.webkit.JavascriptInterface 52 | public void delayJsCallBack(final int ms, final String backMsg, final JsCallback jsCallback) { 53 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 54 | @Override 55 | public void run() { 56 | try { 57 | jsCallback.apply(backMsg); 58 | } catch (JsCallback.JsCallbackException je) { 59 | je.printStackTrace(); 60 | } 61 | } 62 | }, ms); 63 | } 64 | 65 | 那么在网页端的调用如下: 66 | 67 | Android.delayJsCallBack(3 * 1000, 'call back haha', function (msg) { 68 | HostApp.alert(msg); 69 | }); 70 | 71 | 即3秒之后会弹出你传入的'call back haha'信息。 72 | 故从上面的例子我们可以看出,你在网页端定义的回调函数是可以附加多个参数,Java方法在执行回调时需要带入相应的实参就行了。当然这里的**回调函数的参数类型目前还不支持过复杂的类型,仅支持能够被转为字符串的类型**。 73 | 74 | 另外需要注意的是一般传入到Java方法的js function是一次性使用的,即在Java层jsCallback.apply(...)之后不能再发起回调了。如果需要传入的function能够在当前页面生命周期内多次使用,请在第一次apply前**setPermanent(true)**。例如: 75 | 76 | @android.webkit.JavascriptInterface 77 | public void test (JsCallback jsCallback) { 78 | jsCallback.setPermanent(true); 79 | ... 80 | } 81 | 82 | ### 发布时防混淆 83 | 发布时需在你的混淆配置加入像下面这样的代码,注意返回到页面的自定义Java类以及注入类都要**换成你项目中实际使用类名**: 84 | 85 | // 注入到页面的接口类防混淆 86 | -keepclassmembers class android.webkit.safe.sample.JavaScriptInterface{ *; } 87 | 88 | ## License 89 | 90 | The MIT License (MIT) 91 | 92 | Copyright (C) 2015 seven456@gmail.com 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to deal 96 | in the Software without restriction, including without limitation the rights 97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 98 | copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. 111 | -------------------------------------------------------------------------------- /app-sample-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xr0ot/SafeWebView/4ddd6345b1c7b94b89c6dd17f229b1729b4c1e78/app-sample-screenshot.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.1.0' 7 | } 8 | } 9 | 10 | ext { 11 | compileSdkVersion = 17 12 | buildToolsVersion = "19.1.0" 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenCentral() 18 | } 19 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xr0ot/SafeWebView/4ddd6345b1c7b94b89c6dd17f229b1729b4c1e78/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jun 21 11:52:11 CST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileOptions.encoding = "UTF-8" 5 | compileSdkVersion rootProject.ext.compileSdkVersion 6 | buildToolsVersion rootProject.ext.buildToolsVersion 7 | 8 | defaultConfig { 9 | minSdkVersion 9 10 | targetSdkVersion 17 11 | } 12 | 13 | lintOptions { 14 | abortOnError false 15 | } 16 | 17 | // defaultPublishConfig "release" // 当为“release”时,BuildConfig.DEBUG = false; 18 | } 19 | 20 | // apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' -------------------------------------------------------------------------------- /library/doc/injected.js: -------------------------------------------------------------------------------- 1 | (function(b) { 2 | console.log("Android initialization begin"); 3 | var a = { 4 | queue: [], 5 | callback: function() { 6 | var d = Array.prototype.slice.call(arguments, 0); 7 | var c = d.shift(); 8 | var e = d.shift(); 9 | this.queue[c].apply(this, d); 10 | if (!e) { 11 | delete this.queue[c] 12 | } 13 | } 14 | }; 15 | a.delayJsCallBack = a.testLossTime = a.toast = function() { 16 | var f = Array.prototype.slice.call(arguments, 0); 17 | if (f.length < 1) { 18 | throw "Android call error, message:miss method name" 19 | } 20 | var e = []; 21 | for (var h = 1; h < f.length; h++) { 22 | var c = f[h]; 23 | var j = typeof c; 24 | e[e.length] = j; 25 | if (j == "function") { 26 | var d = a.queue.length; 27 | a.queue[d] = c; 28 | f[h] = d 29 | } 30 | } 31 | var k = new Date().getTime(); 32 | var l = f.shift(); 33 | var m = prompt('SafeWebView: ' + JSON.stringify({method: l, types: e, args: f})); 34 | console.log("invoke " + l + ", time: " + (new Date().getTime() - k)); 35 | var g = JSON.parse(m); 36 | if (g.code != 200) { 37 | throw "Android call error, code:" + g.code + ", message:" + g.result 38 | } 39 | return g.result 40 | }; 41 | Object.getOwnPropertyNames(a).forEach(function(d) { 42 | var c = a[d]; 43 | if (typeof c === "function" && d !== "callback") { 44 | a[d] = function() { 45 | return c.apply(a, [d].concat(Array.prototype.slice.call(arguments, 0))) 46 | } 47 | } 48 | }); 49 | b.Android = a; 50 | console.log("Android initialization end") 51 | })(window) -------------------------------------------------------------------------------- /library/doc/notRepeat.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (window.__injectFlag_xxx__) { 3 | console.log('__injectFlag_xxx__ has been injected'); 4 | return; 5 | } 6 | window.__injectFlag_xxx__ = true; 7 | 8 | xxx 9 | 10 | }()) -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/JsCallJava.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | import android.webkit.WebView; 6 | 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.HashMap; 13 | 14 | public class JsCallJava { 15 | private final static String TAG = "JsCallJava"; 16 | private final static String RETURN_RESULT_FORMAT = "{\"code\": %d, \"result\": %s}"; 17 | private static final String MSG_PROMPT_HEADER = "SafeWebView:"; 18 | private static final String KEY_OBJ = "obj"; 19 | private static final String KEY_METHOD = "method"; 20 | private static final String KEY_TYPES = "types"; 21 | private static final String KEY_ARGS = "args"; 22 | private static final String[] IGNORE_UNSAFE_METHODS = {"getClass", "hashCode", "notify", "notifyAll", "equals", "toString", "wait"}; 23 | private HashMap mMethodsMap; 24 | private Object mInterfaceObj; 25 | private String mInterfacedName; 26 | private String mPreloadInterfaceJS; 27 | 28 | public JsCallJava (Object interfaceObj, String interfaceName) { 29 | try { 30 | if (TextUtils.isEmpty(interfaceName)) { 31 | throw new Exception("injected name can not be null"); 32 | } 33 | mInterfaceObj = interfaceObj; 34 | mInterfacedName = interfaceName; 35 | mMethodsMap = new HashMap(); 36 | // getMethods会获得所有继承与非继承的方法 37 | Method[] methods = mInterfaceObj.getClass().getMethods(); 38 | // 拼接的js脚本可参照备份文件:./library/doc/injected.js 39 | StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\""); 40 | sb.append(mInterfacedName); 41 | sb.append(" init begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};"); 42 | for (Method method : methods) { 43 | String sign; 44 | if ((sign = genJavaMethodSign(method)) == null) { 45 | continue; 46 | } 47 | mMethodsMap.put(sign, method); 48 | sb.append(String.format("a.%s=", method.getName())); 49 | } 50 | sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\""); 51 | sb.append(mInterfacedName); 52 | sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h 0) { 156 | Class[] methodTypes = currMethod.getParameterTypes(); 157 | int currIndex; 158 | Class currCls; 159 | while (numIndex > 0) { 160 | currIndex = numIndex - numIndex / 10 * 10 - 1; 161 | currCls = methodTypes[currIndex]; 162 | if (currCls == int.class) { 163 | values[currIndex] = argsVals.getInt(currIndex); 164 | } else if (currCls == long.class) { 165 | //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number 166 | values[currIndex] = Long.parseLong(argsVals.getString(currIndex)); 167 | } else { 168 | values[currIndex] = argsVals.getDouble(currIndex); 169 | } 170 | numIndex /= 10; 171 | } 172 | } 173 | 174 | return getReturn(jsonObject, 200, currMethod.invoke(mInterfaceObj, values), time); 175 | } catch (Exception e) { 176 | //优先返回详细的错误信息 177 | if (e.getCause() != null) { 178 | return getReturn(jsonObject, 500, "method execute error:" + e.getCause().getMessage(), time); 179 | } 180 | return getReturn(jsonObject, 500, "method execute error:" + e.getMessage(), time); 181 | } 182 | } else { 183 | return getReturn(jsonObject, 500, "call data empty", time); 184 | } 185 | } 186 | 187 | private String getReturn (JSONObject reqJson, int stateCode, Object result, long time) { 188 | String insertRes; 189 | if (result == null) { 190 | insertRes = "null"; 191 | } else if (result instanceof String) { 192 | result = ((String) result).replace("\"", "\\\""); 193 | insertRes = "\"".concat(String.valueOf(result)).concat("\""); 194 | } else { // 其他类型直接转换 195 | insertRes = String.valueOf(result); 196 | 197 | // 兼容:如果在解决WebView注入安全漏洞时,js注入采用的是XXX:function(){return prompt(...)}的形式,函数返回类型包括:void、int、boolean、String; 198 | // 在返回给网页(onJsPrompt方法中jsPromptResult.confirm)的时候强制返回的是String类型,所以在此将result的值加双引号兼容一下; 199 | // insertRes = "\"".concat(String.valueOf(result)).concat("\""); 200 | } 201 | String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes); 202 | if (BuildConfig.DEBUG) { 203 | Log.d(TAG, "call time: " + (android.os.SystemClock.uptimeMillis() - time) + ", request: " + reqJson + ", result:" + resStr); 204 | } 205 | return resStr; 206 | } 207 | 208 | private static String promptMsgFormat(String object, String method, String types, String args) { 209 | StringBuilder sb = new StringBuilder(); 210 | sb.append("{"); 211 | sb.append(KEY_OBJ).append(":").append(object).append(","); 212 | sb.append(KEY_METHOD).append(":").append(method).append(","); 213 | sb.append(KEY_TYPES).append(":").append(types).append(","); 214 | sb.append(KEY_ARGS).append(":").append(args); 215 | sb.append("}"); 216 | return sb.toString(); 217 | } 218 | 219 | /** 220 | * 是否是“Java接口类中方法调用”的内部消息; 221 | * @param message 222 | * @return 223 | */ 224 | static boolean isSafeWebViewCallMsg(String message) { 225 | return message.startsWith(MSG_PROMPT_HEADER); 226 | } 227 | 228 | static JSONObject getMsgJSONObject(String message) { 229 | message = message.substring(MSG_PROMPT_HEADER.length()); 230 | JSONObject jsonObject; 231 | try { 232 | jsonObject = new JSONObject(message); 233 | } catch (JSONException e) { 234 | e.printStackTrace(); 235 | jsonObject = new JSONObject(); 236 | } 237 | return jsonObject; 238 | } 239 | 240 | static String getInterfacedName(JSONObject jsonObject) { 241 | return jsonObject.optString(KEY_OBJ); 242 | } 243 | } -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/JsCallback.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Summary: 异步回调页面JS函数管理对象 3 | * Version 1.0 4 | * Date: 13-11-26 5 | * Time: 下午7:55 6 | * Copyright: Copyright (c) 2013 7 | */ 8 | 9 | package android.webkit.safe; 10 | 11 | import android.util.Log; 12 | import android.webkit.WebView; 13 | 14 | import org.json.JSONArray; 15 | import org.json.JSONException; 16 | import org.json.JSONObject; 17 | 18 | import java.lang.ref.WeakReference; 19 | 20 | public class JsCallback { 21 | private static final String CALLBACK_JS_FORMAT = "javascript:%s.callback(%d, %d %s);"; 22 | private int mIndex; 23 | private boolean mCouldGoOn; 24 | private WeakReference mWebViewRef; 25 | private int mIsPermanent; 26 | private String mInjectedName; 27 | 28 | public JsCallback (WebView view, String injectedName, int index) { 29 | mCouldGoOn = true; 30 | mWebViewRef = new WeakReference(view); 31 | mInjectedName = injectedName; 32 | mIndex = index; 33 | } 34 | 35 | /** 36 | * 向网页执行js回调; 37 | * @param args 38 | * @throws JsCallbackException 39 | */ 40 | public void apply (Object... args) throws JsCallbackException { 41 | if (mWebViewRef.get() == null) { 42 | throw new JsCallbackException("the WebView related to the JsCallback has been recycled"); 43 | } 44 | if (!mCouldGoOn) { 45 | throw new JsCallbackException("the JsCallback isn't permanent,cannot be called more than once"); 46 | } 47 | StringBuilder sb = new StringBuilder(); 48 | for (Object arg : args){ 49 | sb.append(","); 50 | boolean isStrArg = arg instanceof String; 51 | // 有的接口将Json对象转换成了String返回,这里不能加双引号,否则网页会认为是String而不是JavaScript对象; 52 | boolean isObjArg = isJavaScriptObject(arg); 53 | if (isStrArg && !isObjArg) { 54 | sb.append("\""); 55 | } 56 | sb.append(String.valueOf(arg)); 57 | if (isStrArg && !isObjArg) { 58 | sb.append("\""); 59 | } 60 | } 61 | String execJs = String.format(CALLBACK_JS_FORMAT, mInjectedName, mIndex, mIsPermanent, sb.toString()); 62 | if (BuildConfig.DEBUG) { 63 | Log.d("JsCallBack", execJs); 64 | } 65 | mWebViewRef.get().loadUrl(execJs); 66 | mCouldGoOn = mIsPermanent > 0; 67 | } 68 | 69 | /** 70 | * 是否是JSON(JavaScript Object Notation)对象; 71 | * @param obj 72 | * @return 73 | */ 74 | private boolean isJavaScriptObject(Object obj) { 75 | if (obj instanceof JSONObject || obj instanceof JSONArray) { 76 | return true; 77 | } else { 78 | String json = obj.toString(); 79 | try { 80 | new JSONObject(json); 81 | } catch (JSONException e) { 82 | try { 83 | new JSONArray(json); 84 | } catch (JSONException e1) { 85 | return false; 86 | } 87 | } 88 | return true; 89 | } 90 | } 91 | 92 | /** 93 | * 一般传入到Java方法的js function是一次性使用的,即在Java层jsCallback.apply(...)之后不能再发起回调了; 94 | * 如果需要传入的function能够在当前页面生命周期内多次使用,请在第一次apply前setPermanent(true); 95 | * @param value 96 | */ 97 | public void setPermanent (boolean value) { 98 | mIsPermanent = value ? 1 : 0; 99 | } 100 | 101 | public static class JsCallbackException extends Exception { 102 | public JsCallbackException (String msg) { 103 | super(msg); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/SafeWebView.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.os.Build; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.ViewParent; 12 | import android.webkit.CookieManager; 13 | import android.webkit.CookieSyncManager; 14 | import android.webkit.JsPromptResult; 15 | import android.webkit.WebChromeClient; 16 | import android.webkit.WebView; 17 | import android.webkit.WebViewClient; 18 | 19 | import org.json.JSONObject; 20 | 21 | import java.lang.reflect.Field; 22 | import java.lang.reflect.Method; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | /** 27 | * Created by zhangguojun on 2015/6/21. 28 | */ 29 | public class SafeWebView extends WebView { 30 | private static final String TAG = "SafeWebView"; 31 | private Map mJsCallJavas; 32 | private Map mInjectJavaScripts; 33 | private SafeWebChromeClient mWebChromeClient; 34 | private SafeWebViewClient mWebViewClient; 35 | 36 | public SafeWebView(Context context) { 37 | this(context, null); 38 | } 39 | 40 | public SafeWebView(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | removeSearchBoxJavaBridge(); 43 | 44 | // WebView跨源(加载本地文件)攻击分析:http://blogs.360.cn/360mobile/2014/09/22/webview%E8%B7%A8%E6%BA%90%E6%94%BB%E5%87%BB%E5%88%86%E6%9E%90/ 45 | // 是否允许WebView使用File协议,移动版的Chrome默认禁止加载file协议的文件; 46 | getSettings().setAllowFileAccess(false); 47 | } 48 | 49 | /** 50 | * 经过大量的测试,按照以下方式才能保证JS脚本100%注入成功: 51 | * 1、在第一次loadUrl之前注入JS(在addJavascriptInterface里面注入即可); 52 | * 2、在webViewClient.onPageStarted中都注入JS; 53 | * 3、在webChromeClient.onProgressChanged中都注入JS,并且不能通过自检查(onJsPrompt里面判断)JS是否注入成功来减少注入JS的次数,因为网页中的JS可以同时打开多个url导致无法控制检查的准确性; 54 | * 4、注入的JS中已经在脚本(./library/doc/notRepeat.js)中检查注入的对象是否已经存在,避免注入对象被重新赋值导致网页引用该对象的方法时发生异常; 55 | * 56 | * @deprecated Android4.2.2及以上版本的addJavascriptInterface方法已经解决了安全问题,如果不使用“网页能将JS函数传到Java层”功能,不建议使用该类,毕竟系统的JS注入效率才是最高的; 57 | */ 58 | @Override 59 | @Deprecated 60 | public void addJavascriptInterface(Object interfaceObj, String interfaceName) { 61 | if (mJsCallJavas == null) { 62 | mJsCallJavas = new HashMap(); 63 | } 64 | mJsCallJavas.put(interfaceName, new JsCallJava(interfaceObj, interfaceName)); 65 | setClient(); 66 | if (mJsCallJavas != null) { 67 | injectJavaScript(); 68 | if (BuildConfig.DEBUG) { 69 | Log.d(TAG, "injectJavaScript, addJavascriptInterface.interfaceObj = " + interfaceObj + ", interfaceName = " + interfaceName); 70 | } 71 | } 72 | } 73 | 74 | @Override 75 | public void setWebViewClient(WebViewClient client) { 76 | if (client instanceof SafeWebViewClient) { 77 | if (mJsCallJavas != null) { 78 | super.setWebViewClient(client); 79 | } else { 80 | mWebViewClient = (SafeWebViewClient) client; 81 | } 82 | } else { 83 | super.setWebViewClient(client); 84 | } 85 | } 86 | 87 | @Override 88 | public void setWebChromeClient(WebChromeClient client) { 89 | if (client instanceof SafeWebChromeClient) { 90 | if (mJsCallJavas != null) { 91 | super.setWebChromeClient(client); 92 | } else { 93 | mWebChromeClient = (SafeWebChromeClient) client; 94 | } 95 | } else { 96 | super.setWebChromeClient(client); 97 | } 98 | } 99 | 100 | @Override 101 | public void destroy() { 102 | if (mJsCallJavas != null) { 103 | mJsCallJavas.clear(); 104 | } 105 | if (mInjectJavaScripts != null) { 106 | mInjectJavaScripts.clear(); 107 | } 108 | removeAllViews(); 109 | //WebView中包含一个ZoomButtonsController,当使用web.getSettings().setBuiltInZoomControls(true);启用该设置后,用户一旦触摸屏幕,就会出现缩放控制图标。这个图标过上几秒会自动消失,但在3.0系统以上上,如果图标自动消失前退出当前Activity的话,就会发生ZoomButton找不到依附的Window而造成程序崩溃,解决办法很简单就是在Activity的ondestory方法中调用web.setVisibility(View.GONE);方法,手动将其隐藏,就不会崩溃了。在3.0一下系统上不会出现该崩溃问题,真是各种崩溃,防不胜防啊! 110 | setVisibility(View.GONE); 111 | ViewParent parent = getParent(); 112 | if (parent instanceof ViewGroup) { 113 | ViewGroup mWebViewContainer = (ViewGroup) getParent(); 114 | mWebViewContainer.removeAllViews(); 115 | } 116 | releaseConfigCallback(); 117 | super.destroy(); 118 | } 119 | 120 | @Override 121 | public void loadUrl(String url) { 122 | if (mJsCallJavas == null) { 123 | setClient(); 124 | } 125 | super.loadUrl(url); 126 | } 127 | 128 | @Override 129 | public void loadUrl(String url, Map additionalHttpHeaders) { 130 | if (mJsCallJavas == null) { 131 | setClient(); 132 | } 133 | super.loadUrl(url, additionalHttpHeaders); 134 | } 135 | 136 | private void setClient() { 137 | if (mWebChromeClient != null) { 138 | setWebChromeClient(mWebChromeClient); 139 | mWebChromeClient = null; 140 | } 141 | if (mWebViewClient != null) { 142 | setWebViewClient(mWebViewClient); 143 | mWebViewClient = null; 144 | } 145 | } 146 | 147 | /** 148 | * 添加并注入JavaScript脚本(和“addJavascriptInterface”注入对象的注入时机一致,100%能注入成功); 149 | * 注意:为了做到能100%注入,需要在注入的js中自行判断对象是否已经存在(如:if (typeof(window.Android) = 'undefined')); 150 | * @param javaScript 151 | */ 152 | public void addInjectJavaScript(String javaScript) { 153 | if (mInjectJavaScripts == null) { 154 | mInjectJavaScripts = new HashMap(); 155 | } 156 | mInjectJavaScripts.put(javaScript.hashCode(), javaScript); 157 | injectExtraJavaScript(); 158 | } 159 | 160 | private void injectJavaScript() { 161 | for (Map.Entry entry : mJsCallJavas.entrySet()) { 162 | this.loadUrl(buildNotRepeatInjectJS(entry.getKey(), entry.getValue().getPreloadInterfaceJS())); 163 | } 164 | } 165 | 166 | private void injectExtraJavaScript() { 167 | for (Map.Entry entry : mInjectJavaScripts.entrySet()) { 168 | this.loadUrl(buildTryCatchInjectJS(entry.getValue())); 169 | } 170 | } 171 | 172 | /** 173 | * 构建一个“不会重复注入”的js脚本; 174 | * @param key 175 | * @param js 176 | * @return 177 | */ 178 | public String buildNotRepeatInjectJS(String key, String js) { 179 | String obj = String.format("__injectFlag_%1$s__", key); 180 | StringBuilder sb = new StringBuilder(); 181 | sb.append("javascript:try{(function(){if(window."); 182 | sb.append(obj); 183 | sb.append("){console.log('"); 184 | sb.append(obj); 185 | sb.append(" has been injected');return;}window."); 186 | sb.append(obj); 187 | sb.append("=true;"); 188 | sb.append(js); 189 | sb.append("}())}catch(e){console.warn(e)}"); 190 | return sb.toString(); 191 | } 192 | 193 | /** 194 | * 构建一个“带try catch”的js脚本; 195 | * @param js 196 | * @return 197 | */ 198 | public String buildTryCatchInjectJS(String js) { 199 | StringBuilder sb = new StringBuilder(); 200 | sb.append("javascript:try{"); 201 | sb.append(js); 202 | sb.append("}catch(e){console.warn(e)}"); 203 | return sb.toString(); 204 | } 205 | 206 | /** 207 | * 如果没有使用addJavascriptInterface方法,不需要使用这个类; 208 | */ 209 | public class SafeWebViewClient extends WebViewClient { 210 | 211 | @Override 212 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 213 | if (mJsCallJavas != null) { 214 | injectJavaScript(); 215 | if (BuildConfig.DEBUG) { 216 | Log.d(TAG, "injectJavaScript, onPageStarted.url = " + view.getUrl()); 217 | } 218 | } 219 | if (mInjectJavaScripts != null) { 220 | injectExtraJavaScript(); 221 | } 222 | super.onPageStarted(view, url, favicon); 223 | } 224 | } 225 | 226 | /** 227 | * 如果没有使用addJavascriptInterface方法,不需要使用这个类; 228 | */ 229 | public class SafeWebChromeClient extends WebChromeClient { 230 | 231 | @Override 232 | public void onProgressChanged(WebView view, int newProgress) { 233 | if (mJsCallJavas != null) { 234 | injectJavaScript(); 235 | if (BuildConfig.DEBUG) { 236 | Log.d(TAG, "injectJavaScript, onProgressChanged.newProgress = " + newProgress + ", url = " + view.getUrl()); 237 | } 238 | } 239 | if (mInjectJavaScripts != null) { 240 | injectExtraJavaScript(); 241 | } 242 | super.onProgressChanged(view, newProgress); 243 | } 244 | 245 | @Override 246 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 247 | if (mJsCallJavas != null && JsCallJava.isSafeWebViewCallMsg(message)) { 248 | JSONObject jsonObject = JsCallJava.getMsgJSONObject(message); 249 | String interfacedName = JsCallJava.getInterfacedName(jsonObject); 250 | if (interfacedName != null) { 251 | JsCallJava jsCallJava = mJsCallJavas.get(interfacedName); 252 | if (jsCallJava != null) { 253 | result.confirm(jsCallJava.call(view, jsonObject)); 254 | } 255 | } 256 | return true; 257 | } else { 258 | return super.onJsPrompt(view, url, message, defaultValue, result); 259 | } 260 | } 261 | } 262 | 263 | // 解决WebView内存泄漏问题; 264 | private void releaseConfigCallback() { 265 | if (android.os.Build.VERSION.SDK_INT < 16) { // JELLY_BEAN 266 | try { 267 | Field field = WebView.class.getDeclaredField("mWebViewCore"); 268 | field = field.getType().getDeclaredField("mBrowserFrame"); 269 | field = field.getType().getDeclaredField("sConfigCallback"); 270 | field.setAccessible(true); 271 | field.set(null, null); 272 | } catch (NoSuchFieldException e) { 273 | if (BuildConfig.DEBUG) { 274 | e.printStackTrace(); 275 | } 276 | } catch (IllegalAccessException e) { 277 | if (BuildConfig.DEBUG) { 278 | e.printStackTrace(); 279 | } 280 | } 281 | } else { 282 | try { 283 | Field sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback"); 284 | if (sConfigCallback != null) { 285 | sConfigCallback.setAccessible(true); 286 | sConfigCallback.set(null, null); 287 | } 288 | } catch (NoSuchFieldException e) { 289 | if (BuildConfig.DEBUG) { 290 | e.printStackTrace(); 291 | } 292 | } catch (ClassNotFoundException e) { 293 | if (BuildConfig.DEBUG) { 294 | e.printStackTrace(); 295 | } 296 | } catch (IllegalAccessException e) { 297 | if (BuildConfig.DEBUG) { 298 | e.printStackTrace(); 299 | } 300 | } 301 | } 302 | } 303 | 304 | /** 305 | * Android 4.4 KitKat 使用Chrome DevTools 远程调试WebView 306 | * WebView.setWebContentsDebuggingEnabled(true); 307 | * http://blog.csdn.net/t12x3456/article/details/14225235 308 | */ 309 | @TargetApi(19) 310 | protected void trySetWebDebuggEnabled() { 311 | if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= 19) { 312 | try { 313 | Class clazz = WebView.class; 314 | Method method = clazz.getMethod("setWebContentsDebuggingEnabled", boolean.class); 315 | method.invoke(null, true); 316 | } catch (Throwable e) { 317 | if (BuildConfig.DEBUG) { 318 | e.printStackTrace(); 319 | } 320 | } 321 | } 322 | } 323 | 324 | /** 325 | * 解决Webview远程执行代码漏洞,避免被“getClass”方法恶意利用(在loadUrl之前调用,如:MyWebView(Context context, AttributeSet attrs)里面); 326 | * 漏洞详解:http://drops.wooyun.org/papers/548 327 | *

328 | * function execute(cmdArgs) 329 | * { 330 | * for (var obj in window) { 331 | * if ("getClass" in window[obj]) { 332 | * alert(obj); 333 | * return ?window[obj].getClass().forName("java.lang.Runtime") 334 | * .getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); 335 | * } 336 | * } 337 | * } 338 | * 339 | * @return 340 | */ 341 | @TargetApi(11) 342 | protected boolean removeSearchBoxJavaBridge() { 343 | try { 344 | if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) { 345 | Method method = this.getClass().getMethod("removeJavascriptInterface", String.class); 346 | method.invoke(this, "searchBoxJavaBridge_"); 347 | return true; 348 | } 349 | } catch (Exception e) { 350 | if (BuildConfig.DEBUG) { 351 | e.printStackTrace(); 352 | } 353 | } 354 | return false; 355 | } 356 | 357 | /** 358 | * 解决Android4.2中开启了辅助模式后,LocalActivityManager控制的Activity与AccessibilityInjector不兼容导致的崩溃问题; 359 | * Caused by: java.lang.NullPointerException 360 | * at android.webkit.AccessibilityInjector$TextToSpeechWrapper$1.onInit(AccessibilityInjector.java:753) 361 | * ... 362 | * at android.webkit.WebSettingsClassic.setJavaScriptEnabled(WebSettingsClassic.java:1125) 363 | * 必须放在webSettings.setJavaScriptEnabled之前执行; 364 | */ 365 | protected void fixedAccessibilityInjectorException() { 366 | if (Build.VERSION.SDK_INT == 17) { 367 | try { 368 | Object webViewProvider = WebView.class.getMethod("getWebViewProvider").invoke(this); 369 | Method getAccessibilityInjector = webViewProvider.getClass().getDeclaredMethod("getAccessibilityInjector"); 370 | getAccessibilityInjector.setAccessible(true); 371 | Object accessibilityInjector = getAccessibilityInjector.invoke(webViewProvider); 372 | getAccessibilityInjector.setAccessible(false); 373 | Field mAccessibilityManagerField = accessibilityInjector.getClass().getDeclaredField("mAccessibilityManager"); 374 | mAccessibilityManagerField.setAccessible(true); 375 | Object mAccessibilityManager = mAccessibilityManagerField.get(accessibilityInjector); 376 | mAccessibilityManagerField.setAccessible(false); 377 | Field mIsEnabledField = mAccessibilityManager.getClass().getDeclaredField("mIsEnabled"); 378 | mIsEnabledField.setAccessible(true); 379 | mIsEnabledField.set(mAccessibilityManager, false); 380 | mIsEnabledField.setAccessible(false); 381 | } catch (Exception e) { 382 | if (BuildConfig.DEBUG) { 383 | e.printStackTrace(); 384 | } 385 | } 386 | } 387 | } 388 | 389 | /** 390 | * 向网页更新Cookie,设置cookie后不需要页面刷新即可生效; 391 | */ 392 | protected void updateCookies(String url, String value) { 393 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { // 2.3及以下 394 | CookieSyncManager.createInstance(getContext().getApplicationContext()); 395 | } 396 | CookieManager cookieManager = CookieManager.getInstance(); 397 | cookieManager.setAcceptCookie(true); 398 | cookieManager.setCookie(url, value); 399 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { // 2.3及以下 400 | CookieSyncManager.getInstance().sync(); 401 | } 402 | } 403 | } -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileOptions.encoding = "UTF-8" 5 | 6 | compileSdkVersion rootProject.ext.compileSdkVersion 7 | buildToolsVersion rootProject.ext.buildToolsVersion 8 | 9 | defaultConfig { 10 | minSdkVersion 9 11 | targetSdkVersion 17 12 | } 13 | 14 | lintOptions { 15 | abortOnError false 16 | } 17 | 18 | buildTypes { 19 | release { 20 | proguardFile 'proguard-android.txt' 21 | minifyEnabled true 22 | shrinkResources true 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | //compile 'cn.pedant.safewebviewbridge:library:1.4' 29 | compile project(':library') 30 | } -------------------------------------------------------------------------------- /sample/proguard-android.txt: -------------------------------------------------------------------------------- 1 | #--------------- BEGIN: Gson防混淆 ---------- 2 | -keepattributes *Annotation* 3 | -keep class sun.misc.Unsafe { *; } 4 | -keep class com.idea.fifaalarmclock.entity.*** 5 | -keep class com.google.gson.stream.** { *; } 6 | #--------------- END ---------- 7 | 8 | #--------------- BEGIN: 返回给页面的自定义Java对象防混淆 ---------- 9 | -keepclassmembers class android.webkit.safe.sample.JavaScriptInterface$RetJavaObj{ *; } 10 | #--------------- END ---------- 11 | 12 | #--------------- BEGIN: 注入到页面的接口类防混淆 ---------- 13 | -keepclassmembers class android.webkit.safe.sample.JavaScriptInterface{ *; } 14 | #--------------- END ---------- -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/assets/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SafeWebView 6 | 7 | 8 | 36 | 37 | 38 | 39 |

40 |

Java Interface 接口测试

41 |

WebView漏洞测试(Android4.2.2以下)

42 |
43 |
    44 |
  • 45 | 气泡提示
    46 | 代码:
    47 |
    48 | Android.toast('我是气泡'); 49 |
    50 | 51 |
  • 52 | 53 |
    54 |
  • 55 | 测试代码执行时间
    56 | 代码:
    57 |
    58 | Android.testLossTime(new Date().getTime()); 59 |
    60 | 61 |
  • 62 | 63 |
    64 |
  • 65 | 异步回调,传入js函数到Java方法,设定3秒后回调
    66 | 代码:
    67 |
    68 | Android.delayJsCallBack(3 * 1000, 'call back haha', function (msg) { 69 | Android.toast(msg); 70 | }); 71 |
    72 | 73 |
  • 74 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /sample/src/main/assets/unsafe_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 41 | 42 | 43 | 44 |

WebView漏洞测试(Android4.2.2以下版本可测)-(ls -l /mnt/sdcard/)

45 | 46 | 47 | -------------------------------------------------------------------------------- /sample/src/main/java/android/webkit/safe/sample/JavaScriptInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Summary: js脚本所能执行的函数空间 3 | * Version 1.0 4 | * Date: 13-11-20 5 | * Time: 下午4:40 6 | * Copyright: Copyright (c) 2013 7 | */ 8 | 9 | package android.webkit.safe.sample; 10 | 11 | import android.os.Handler; 12 | import android.os.Looper; 13 | import android.webkit.WebView; 14 | import android.widget.Toast; 15 | 16 | import android.webkit.safe.JsCallback; 17 | 18 | public class JavaScriptInterface { 19 | private WebView mWebView; 20 | 21 | public JavaScriptInterface(WebView webView) { 22 | mWebView = webView; 23 | } 24 | 25 | /** 26 | * 短暂气泡提醒 27 | * 28 | * @param message 提示信息 29 | */ 30 | @android.webkit.JavascriptInterface 31 | public void toast(String message) { 32 | Toast.makeText(mWebView.getContext(), message, Toast.LENGTH_SHORT).show(); 33 | } 34 | 35 | /** 36 | * 弹出记录的测试JS层到Java层代码执行损耗时间差 37 | * 38 | * @param timeStamp js层执行时的时间戳 39 | */ 40 | @android.webkit.JavascriptInterface 41 | public void testLossTime(long timeStamp) { 42 | timeStamp = System.currentTimeMillis() - timeStamp; 43 | toast(String.valueOf(timeStamp)); 44 | } 45 | 46 | @android.webkit.JavascriptInterface 47 | public void delayJsCallBack(final int ms, final String backMsg, final JsCallback jsCallback) { 48 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 49 | @Override 50 | public void run() { 51 | try { 52 | jsCallback.apply(backMsg); 53 | } catch (JsCallback.JsCallbackException je) { 54 | je.printStackTrace(); 55 | } 56 | } 57 | }, ms); 58 | } 59 | } -------------------------------------------------------------------------------- /sample/src/main/java/android/webkit/safe/sample/UnsafeWebActivity.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe.sample; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.webkit.JsResult; 8 | import android.webkit.WebChromeClient; 9 | import android.webkit.WebSettings; 10 | import android.webkit.WebView; 11 | 12 | public class UnsafeWebActivity extends Activity { 13 | public static final String HTML = "file:///android_asset/unsafe_test.html"; 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | WebView wv = new WebView(this); 19 | setContentView(wv); 20 | 21 | WebSettings webView = wv.getSettings(); 22 | webView.setJavaScriptEnabled(true); 23 | wv.setWebChromeClient(new InnerChromeClient()); 24 | 25 | wv.loadUrl(HTML); 26 | } 27 | 28 | public class InnerChromeClient extends WebChromeClient { 29 | 30 | @Override 31 | public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { 32 | new AlertDialog.Builder(view.getContext()) 33 | .setMessage(message) 34 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 35 | @Override 36 | public void onClick(DialogInterface dialog, int which) { 37 | result.confirm(); 38 | } 39 | }) 40 | .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 41 | @Override 42 | public void onClick(DialogInterface dialog, int which) { 43 | result.cancel(); 44 | } 45 | }) 46 | .create() 47 | .show(); 48 | return true; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /sample/src/main/java/android/webkit/safe/sample/WebActivity.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe.sample; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.webkit.JsPromptResult; 8 | import android.webkit.WebSettings; 9 | import android.webkit.WebView; 10 | import android.webkit.WebViewClient; 11 | import android.webkit.safe.SafeWebView; 12 | 13 | public class WebActivity extends Activity { 14 | public static final String HTML = "file:///android_asset/test.html"; 15 | 16 | @Override 17 | public void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | WebView webView = new InnerWebView(this); 20 | 21 | setContentView(webView); 22 | 23 | webView.loadUrl(HTML); 24 | } 25 | 26 | private class InnerWebView extends SafeWebView { 27 | 28 | public InnerWebView(Context context) { 29 | super(context); 30 | 31 | WebSettings ws = getSettings(); 32 | ws.setJavaScriptEnabled(true); 33 | addJavascriptInterface(new JavaScriptInterface(this), "Android"); 34 | setWebChromeClient(new InnerWebChromeClient()); 35 | setWebViewClient(new InnerWebViewClient()); 36 | } 37 | 38 | public class InnerWebChromeClient extends SafeWebChromeClient { 39 | 40 | @Override 41 | public void onProgressChanged (WebView view, int newProgress) { 42 | super.onProgressChanged(view, newProgress); // 务必放在方法体的第一行执行; 43 | // to do your work 44 | // ... 45 | } 46 | 47 | @Override 48 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 49 | // to do your work 50 | // ... 51 | return super.onJsPrompt(view, url, message, defaultValue, result); // 务必放在方法体的最后一行执行,或者用if判断也行; 52 | } 53 | } 54 | 55 | private class InnerWebViewClient extends WebViewClient { 56 | @Override 57 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 58 | if (url.equals(UnsafeWebActivity.HTML)) { 59 | view.getContext().startActivity(new Intent(view.getContext(), UnsafeWebActivity.class)); 60 | return true; 61 | } 62 | return super.shouldOverrideUrlLoading(view, url); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xr0ot/SafeWebView/4ddd6345b1c7b94b89c6dd17f229b1729b4c1e78/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xr0ot/SafeWebView/4ddd6345b1c7b94b89c6dd17f229b1729b4c1e78/sample/src/main/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xr0ot/SafeWebView/4ddd6345b1c7b94b89c6dd17f229b1729b4c1e78/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xr0ot/SafeWebView/4ddd6345b1c7b94b89c6dd17f229b1729b4c1e78/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SafeWebView 4 | 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library', 'sample' --------------------------------------------------------------------------------