Webview - 小优化 & 安全漏洞及修复

1,240 阅读12分钟

转载自:

作者:AWeiLoveAndroid

链接:www.jianshu.com/p/2b2e5d417…

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

关于WebView的一点小优化:

(1)给WebView加一个加载进度条

  用Webview加载一个网页时,如果加载时间长,界面会一直空白,体验不太好,所以加个进度条更好看一下,主流APP也都有进度条效果,大概思路我来说一下:   首先自定义一个HorizontalProgressView继承View,然后自定义一个MyWebView继承WebView,然后初始化的时候通过addView方法把前面自定义HorizontalProgressView,然后在MyWebView里面写一个内部类继承WebChromeClient,大致代码如下:

private class MyWebCromeClient extends WebChromeClient {
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        if (newProgress == 100) {
            //加载完毕进度条消失
            progressView.setVisibility(View.GONE);
        } else {
            //更新进度
            progressView.setProgress(newProgress);
        }
        super.onProgressChanged(view, newProgress);
    }
}
  • 主要是通过MyWebCromeClient 的onProgressChanged方法里面的进度值调用 progressView.setProgress()方法去更新进度条,当加载100%的时候让进度条消失。 具体实现你们自己去处理吧。

(2)加快HTML网页加载完成的速度,等页面finish再加载图片

  默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。

处理方案在WebView初始化时设置如下代码:

public void int () {
    if(Build.VERSION.SDK_INT >= 19) {
        webView.getSettings().setLoadsImagesAutomatically(true);
    } else {
        webView.getSettings().setLoadsImagesAutomatically(false);
    }
}

同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:

@Override
public void onPageFinished(WebView view, String url) {
    if(!webView.getSettings().getLoadsImagesAutomatically()) {
        webView.getSettings().setLoadsImagesAutomatically(true);
    }
}

(3)自定义WebView页面加载出错界面

  当WebView加载页面出错时(一般为404 NOT FOUND),安卓WebView会默认显示出错界面。当WebView加载出错时,我们会在WebViewClient实例中的onReceivedError()方法接收到错误,我们就在这里做些手脚:

@Override
public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) {
    super.onReceivedError(view, errorCode, description, failingUrl);
    loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
    mErrorFrame.setVisibility(View.VISIBLE);
}

  从上面可以看出,我们先使用loadDataWithBaseURL清除掉默认错误页内容,再让我们自定义的View得到显示(mErrorFrame为蒙在WebView之上的一个LinearLayout布局,默认为View.GONE)。

(4) 怎么知道WebView是否已经滚动到页面底端?

方案1,使用原生WebView的api可以获取到:
    if (mWebView.getContentHeight() * mWebView.getScale()  == (mWebView.getHeight() + 
    mWebView.getScrollY())) {
        //说明已经到底了
    }
方案2,继承WebView,重写onScrollChanged方法:
  我们在做上拉加载下一页这样的功能时,也需要知道当前页面滚动条所处的状态,如果快到底部,则要发起网络请求数据更新网页。同样继承WebView类,在子类覆盖onScrollChanged方法。\
  以下代码中mCurrContentHeight用于记录上次触发时的网页高度,用来防止在网页总高度未发生变化而目标区域发生连续滚动时会多次触发TODO,mThreshold是一个阈值,当页面底部距离滚动条底部的高度差<=这个值时会触发TODO里面的代码。

具体如下:

@Override
protected void onScrollChanged(int newX, int newY, int oldX, int oldY) {
    super.onScrollChanged(newX, newY, oldX, oldY);
    if (newY != oldY) {
        float contentHeight = getContentHeight() * getScale();
        // 当前内容高度下从未触发过, 浏览器存在滚动条且滑动到将抵底部位置
        if (mCurrContentHeight != contentHeight && newY > 0 && contentHeight <= newY + getHeight() + mThreshold) {
            // TODO Something...
            mCurrContentHeight = contentHeight;
        }
    }
}
getContentHeight() @return the height of the HTML content
●   getScale() @return the current scale
●   getHeight() @return The height of your view
●   getScrollY() @return The top edge of the displayed part of your view, in pixels.

(5) 怎么知道WebView是否存在滚动条?

  当我们做类似上拉加载下一页这样的功能的时候,页面初始的时候需要知道当前WebView是否存在纵向滚动条,如果有则不加载下一页,如果没有则加载下一页直到其出现纵向滚动条。   首先继承WebView类,在子类添加下面的代码:

public boolean existVerticalScrollbar () {
    return computeVerticalScrollRange() > computeVerticalScrollExtent();
}

  computeVerticalScrollRange得到的是可滑动的最大高度,computeVerticalScrollExtent得到的是滚动把手自身的高,当不存在滚动条时,两者的值是相等的。当有滚动条时前者一定是大于后者的。

WebView的使用漏洞及其修复方式

WebView中,主要漏洞有三类:

1.任意代码执行漏洞
2.密码明文存储漏洞
3.域控制不严格漏洞

(一)任意代码执行漏洞**

(1)addJavascriptInterface 接口引起远程代码执行漏洞

  • 产生原因:
    js调用Android的其中一个方式是通过addJavascriptInterface接口进行对象映射:

    webView.addJavascriptInterface(new JSObject(), "myObj");
    // 参数1:Android的本地对象
    // 参数2:JS的对象
    // 通过对象映射将Android中的本地对象和JS中的对象进行关联,从而实现JS调用Android的对象和方法
    所以,漏洞产生原因是:当JS拿到android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(Java.lang.Runtime 类),
从而进行任意代码执行。(比如**我们可以执行命令获取本地设备的SD卡中的文件等信息从而造成信息泄露**)

具体获取系统类的描述:(结合 Java 反射机制)

    1. Android中的对象有一公共的方法:getClass() ;
    1. 该方法可以获取到当前类 类型Class
    1. 该类有一关键的方法: Class.forName;
    1. 该方法可以加载一个类(可加载 java.lang.Runtime 类)
    1. 而该类是可以执行本地命令的

以下是攻击的Js核心代码:

function execute(cmdArgs)  {  
    // 步骤1:遍历 window 对象
    // 目的是为了找到包含 getClass ()的对象
    // 因为Android映射的JS对象也在window中,所以肯定会遍历到
    for (var obj in window) {  
        if ("getClass" in window[obj]) {  
            // 步骤2:利用反射调用forName()得到Runtime类对象
            alert(obj);          
            return  window[obj].getClass().forName("java.lang.Runtime")  
            // 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
            getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  
            // 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
            // 如执行完访问文件的命令之后,就可以得到文件名的信息了。
        }  
    }  
} 

当一些 APP 通过扫描二维码打开一个外部网页时,攻击者就可以执行这段 js 代码进行漏洞攻击。在微信盛行、扫一扫行为普及的情况下,该漏洞的危险性非常大

  • 解决方法

Android 4.2版本之后:Google 在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击

Android 4.2版本之前:采用拦截prompt()进行漏洞修复。 具体步骤如下(了解即可):

1.继承 WebView ,重写 addJavascriptInterface 方法,然后在内部自己维护一个对象映射关系的 Map ( 将需要添加的 JS 接口放入该Map中 )
2.每次当 WebView 加载页面前加载一段本地的 JS 代码,原理是:
    1) 让JS调用一Javascript方法:该方法是通过调用prompt()把JS中的信息(含特定标识,方法名称等)传递到Android端;
    2) 在Android的onJsPrompt()中 ,解析传递过来的信息,再通过反射机制调用Java对象的方法,这样实现安全的JS调用Android代码。
关于Android返回给JS的值:可通过prompt()把Java中方法的处理结果返回到Js

具体需要加载的JS代码如下:

 javascript:(function JsAddJavascriptInterface_(){  
    // window.jsInterface 表示在window上声明了一个Js对象
    // jsInterface = 注册的对象名
    // 它注册了两个方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
    // 如果有返回值,就添加上return
    if (typeof(window.jsInterface)!='undefined') {      
        console.log('window.jsInterface_js_interface_name is exist!!');}   
    else {  
        window.jsInterface = {     
            // 声明方法形式:方法名: function(参数)
            onButtonClick:function(arg0) {   
            // prompt()返回约定的字符串
            // 该字符串可自己定义
            // 包含特定的标识符MyApp和 JSON 字符串(方法名,参数,对象名等)    
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
            },  
            onImageClick:function(arg0,arg1,arg2) {   
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',
args:[arg0,arg1,arg2]}));  
            },  
        };  
    }  
}  
)()
// 当JS调用 onButtonClick() 或 onImageClick() 时,就会回调到Android中的 onJsPrompt ()
// 我们解析出方法名,参数,对象名
// 再通过反射机制调用Java对象的方法

关于采用拦截prompt()进行漏洞修复需要注意的两点细节:

细节1:加载上述JS代码的时机

由于当 WebView 跳转到下一个页面时,之前加载的 JS 可能已经失效,所以,通常需要在以下方法中加载js:
    onLoadResource();
    doUpdateVisitedHistory();
    onPageStarted();
    onPageFinished();
    onReceivedTitle();
    onProgressChanged();

细节2:需要过滤掉 Object 类的方法

由于最终是通过反射得到Android指定对象的方法,所以同时也会得到基类的其他方法(最顶层的基类是 Object类)
为了不把 getClass()等方法注入到 JS 中,我们需要把 Object 的共有方法过滤掉,需要过滤的方法列表如下:
    getClass()
    hashCode()
    notify()
    notifyAl()
    equals()
    toString()
    wait()

(2)searchBoxJavaBridge_接口引起远程代码执行漏洞

  • 产生原因
1) 在Android 3.0以下,Android系统会默认通过searchBoxJavaBridge_的Js接口给 WebView 添加一个JS映射对象:
searchBoxJavaBridge_对象
2) 该接口可能被利用,实现远程任意代码。
  • 解决方法
删除searchBoxJavaBridge_接口
// 通过调用该方法删除接口removeJavascriptInterface();

(二)密码明文存储漏洞

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

(三)域控制不严格漏洞

先看Android里的WebViewActivity.java:

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().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);
    }
 }

/**Mainifest.xml**/
// 将该 WebViewActivity 在Mainifest.xml设置exported属性
// 表示:当前Activity是否可以被另一个Application的组件启动
android:exported="true"
  • 问题分析上述demo中:即 A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁

具体: 当其他应用启动此 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),其他 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意url。

  • 下面我们着重分析WebView中getSettings类的方法对 WebView 安全性的影响:
setAllowFileAccess()
setAllowFileAccessFromFileURLs()//设置为FALSE
setAllowUniversalAccessFromFileURLs()//设置为FALSE
setAllowFileAccess()
// 设置是否允许 WebView 使用 File 协议,默认设置为true,
// 即允许在 File 域下执行任意 JavaScript 代码
webView.getSettings().setAllowFileAccess(true);     

但是同时也限制了 WebView 的功能,使其不能加载本地的 html 文件,( 移动版的 Chrome 默认禁止加载 file 协议的文件 )

解决方案:

1) 对于不需要使用 file 协议的应用,禁用 file 协议;
  setAllowFileAccess(false); 
  
2) 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
  setAllowFileAccess(true); 
  // 禁止 file 协议加载 JavaScript
  if (url.startsWith("file://") {
      setJavaScriptEnabled(false);
  } else {
      setJavaScriptEnabled(true);
  }
setAllowFileAccessFromFileURLs()

设置是否允许通过 file url 加载的 Js代码读取其他的本地文件 , 在Android 4.1前默认允许 , 在Android 4.1后默认禁止

webView.getSettings().setAllowFileAccessFromFileURLs(true);

当AllowFileAccessFromFileURLs()设置为 true 时,攻击者的JS代码为 ( 通过该代码可成功读取 /etc/hosts 的内容数据 )

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

当设置成为 false 时,上述JS的攻击代码执行会导致错误,表示浏览器禁止从 file url 中的 JavaScript 读取其它本地文件。

setAllowUniversalAccessFromFileURLs()

设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源),在Android 4.1前默认允许(setAllowFileAccessFromFileURLs()不起作用),在Android 4.1后默认禁止

**

webView.getSettings().setAllowUniversalAccessFromFileURLs(true);

当AllowFileAccessFromFileURLs()被设置成true时,攻击者的JS代码是:

// 通过该代码可成功读取 http://www.so.com 的内容
<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>
setJavaScriptEnabled()

设置是否允许 WebView 使用 JavaScript(默认是不允许),但很多应用(包括移动浏览器)为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的,如下代码所示:

webView.getSettings().setJavaScriptEnabled(true);  

即使把setAllowFileAccessFromFileURLs()和setAllowUniversalAccessFromFileURLs()都设置为 false,通过 file URL 加载的 javascript仍然有方法访问其他的本地文件:符号链接跨源攻击(前提是允许 file URL 执行 javascript,即webView.getSettings().setJavaScriptEnabled(true);)

原因分析: 这一攻击能奏效的原因是:通过 javascript 的延时执行和 将当前文件替换成指向其它文件的软链接就可以读取到被 符号链接所指的文件。

具体攻击步骤:(在该命令执行前 xx.html 是不存在的;执行完这条命令之后,就生成了这个文件,并且将 Cookie 文件链接到了 xx.html 上。)\
1. 把恶意的 js 代码输出到攻击应用的目录下,随机命名为 xx.html,修改该目录的权限;\
2. 修改后休眠 1s,让文件操作完成;\
3. 完成后通过系统的 Chrome 应用去打开该 xx.html 文件\
4. 等待 4s 让 Chrome 加载完成该 html,最后将该 html 删除,并且使用 ln -s 命令为 Chrome 的 Cookie 文件创建软连接,\
于是就可通过链接来访问 Chrome 的 Cookie
  • 注意事项:   1、Google 没有进行修复,只是让Chrome 最新版本默认禁用 file 协议,所以这一漏洞在最新版的 Chrome 中并不存在。

  2、但是,在日常大量使用 WebView 的App和浏览器,都有可能受到此漏洞的影响。通过利用此漏洞,容易出现数据泄露的危险。如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁。

  3、但并不能完全杜绝跨源文件泄露。例:应用实现了下载功能,对于无法加载的页面,会自动下载到 sd 卡中;由于 sd 卡中的文件所有应用都可以访问,于是可以通过构造一个 file URL 指向被攻击应用的私有文件,然后用此 URL 启动被攻击应用的 WebActivity,这样由于该 WebActivity 无法加载该文件,就会将该文件下载到 sd 卡下面,然后就可以从 sd 卡上读取这个文件了

  • 最终解决方案

1)对于不需要使用 file 协议的应用,禁用 file 协议;

// 禁用 file 协议;
webView.setAllowFileAccess(false); 
webView.setAllowFileAccessFromFileURLs(false);
webView.setAllowUniversalAccessFromFileURLs(false);

2)对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。

// 需要使用 file 协议
webView.setAllowFileAccess(true); 
webView.setAllowFileAccessFromFileURLs(false);
webView. setAllowUniversalAccessFromFileURLs(false);

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    webView.setJavaScriptEnabled(false);
} else {
    webView.setJavaScriptEnabled(true);
}

其他:

WebView-基础篇

WebView-填坑之路

WebView-# 缓存机制 & 资源预加载方案