1. 程式人生 > >android WebView詳解,常見漏洞詳解和安全原始碼(上)

android WebView詳解,常見漏洞詳解和安全原始碼(上)

  這篇部落格主要來介紹 WebView 的相關使用方法,常見的幾個漏洞,開發中可能遇到的坑和最後解決相應漏洞的原始碼,以及針對該原始碼的解析。
  由於部落格內容長度,這次將分為上下兩篇,上篇詳解 WebView 的使用,下篇講述 WebView 的漏洞和坑,以及修復原始碼的解析。
  下篇:android WebView詳解,常見漏洞詳解和安全原始碼(下)
  轉載請註明出處:http://blog.csdn.net/self_study/article/details/54928371
  對技術感興趣的同鞋加群 544645972 一起交流。

Android Hybrid 和 WebView 解析

  現在市面上的 APP 根據型別大致可以分為 3 類:Native APP、Web APP 和 Hybrid APP,而 Hybrid APP 兼具 “Native APP 良好使用者互動體驗的優勢”和 “Web APP 跨平臺開發的優勢”,現在很多的主流應用也是使用 Hybrid 模式開發的。

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 在很多情況下相比差距還是不小的,但是這種問題從某種程度上來說也是可以彌補的,比如說我們可以做一些資源預載入的方案,在資源預載入方面,其實也有很多種方式,下面主要列舉了一些:

  • 第一種方式是使用 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 的介紹:

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() 方法讓我們的程式可以進行遠端桌面除錯。

WebView 載入頁面

  WebView 有四個用來載入頁面的方法:

  使用起來較為簡單,loadData 方法會有一些坑,在下面的內容會介紹到。

WebView 常見設定

  使用 WebView 的時候,一般都會對其進行一些設定,我們來看看常見的設定:

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執行處理各種響應請求事件的,比如:

  • 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 進行互動,下面介紹一下互動的方式。

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".
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 常見漏洞和坑

原始碼

引用