android WebView详解,常见漏洞详解和安全源码(下)
上篇博客主要分析了 WebView 的詳細使用,這篇來分析 WebView 的常見漏洞和使用的坑。?
上篇:android WebView詳解,常見漏洞詳解和安全源碼(上)?
轉載請注明出處:http://blog.csdn.net/self_study/article/details/55046348?
對技術感興趣的同鞋加群 544645972 一起交流。
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 腳本的代碼如下所示:
- 1
- 2
- 3
- 4
- 5
CVE-2012-6636?和?CVE-2013-4710
Android 系統為了方便 APP 中 Java 代碼和網頁中的 Javascript 腳本交互,在 WebView 控件中實現了 addJavascriptInterface 接口,如上面的代碼所示,我們來看一下這個方法的官方描述:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 1
- 2
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
所以當一些 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 代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
這段 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);}}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
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.- 1
- 2
- 3
通過這個 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.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
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>- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
以上漏洞的初步解決方案
通過以上的介紹,初步的方案是使用下面的代碼來杜絕:
setAllowFileAccess(true); //設置為 false 將不能加載本地 html 文件 setAllowFileAccessFromFileURLs(false); setAllowUniversalAccessFromFileURLs(false);- 1
- 2
- 3
使用符號鏈接跨源
為了安全的使用 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 出來了,實際情況可以上傳到服務器,攻擊的詳細代碼如下所示:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
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
- 2
- 3
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
開發中遇見的坑
這里記錄一下開發中遇到的一些坑和解決辦法:
loadData() 方法
我們可以通過使用?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");- 1
- 2
這里的邏輯很簡單,加載一個簡單的富文本標簽,我們看看運行后的效果:
可以注意到這里顯示成亂碼了,可是明明已經指定了編碼格式為 UTF-8 啊,可是這就是使用的坑,我們需要將代碼進行修改:
String html = new String("<h3>我是loadData() 的標題</h3><p>  我是他的內容</p>"); webView.loadData(html, "text/html;charset=UTF-8", "null");- 1
- 2
我們再來看看顯示效果:
這樣我們就可以看到正確的內容了,Google 還指出,在我們這種加載的方法下,我們的 Data 數據里不能出現 ’#’, ‘%’, ‘\’ , ‘?’ 這四個字符,如果出現了我們要用 %23, %25, %27, %3f 對應來替代,網上列舉了未將特定字符轉義過程中遇到的異?,F象:
A) % 會報找不到頁面錯誤,頁面全是亂碼。 B) # 會讓你的 goBack 失效,但 canGoBAck 是可以使用的,于是就會產生返回按鈕生效,但不能返回的情況。 C) \ 和 ? 在轉換時,會報錯,因為它會把 \ 當作轉義符來使用,如果用兩級轉義,也不生效。- 1
- 2
- 3
頁面空白
當 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 即可。
4.4 版本之后 loadUrl 加載 js 傳遞 url 自動轉義
在開發中遇到過一個需求是在 WebView 中需要調用前端的 js 腳本處理一段 url,js 解析完這段 url 之后再把結果交由本地進行處理,但是遇到一個問題是,比如一個 url 為 “https://www.aaaa.com/bb?param1=3333%23444¶m2=555%40666“,大家都知道 url 中如果存在類似“#@”這種特殊符號的時候就需要 encode 成 23% 和 40%,這樣才是一個符合要求的 url,要不然 “https://www.aaaa.com/bb?param1=3333#444¶m2=555@666”這個 url 是一個非法的 url,但是當時將這段合法的 url 通過 loadUrl(“javascript:xxxxx”) 的方式傳遞給 js 的相關函數進行處理,js 端獲取到 url 被自動轉義成了“https://www.aaaa.com/bb?param1=3333#444¶m2=555@666“,去 google 了很久才發現這個問題原來是 android 自帶的一個問題,見?issue:36995865,原來在 4.4 之前系統的 WebView 不會自動 decode,但是 4.4 和之后的系統上通過 loadUrl 傳遞的東西會自動將 %23 等 decode 成 #,這樣就造成在 4.4 之后通過 loadUrl 加載一段 js,傳遞一段 url,如果 url 里面有 # @ 等非法字符的時候就會造成 js 端獲取到的 url 非法,無法正常解析,解決辦法就是在 4.4 版本之后使用 evaluateJavascript 這個函數,這個函數也正好是 4.4 版本引入的,使用 evaluateJavascript 這個函數傳遞 url,就不會自動 decode,js 函數獲取到的 url 仍然是轉義后的 %23 %40,這個問題雖然很少人遇到,但是遇到了就屬于一個需要時間定位和處理的問題了。
源碼及解析
來看看解決上述問題的 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;}@Overridepublic 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\'");}}}@Overridepublic 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")@Overridepublic 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();//HEADStringBuilder 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();}// Endscript.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 implementationif (method.getReturnType() != void.class) {script.append(" return ").append("prompt('").append(MSG_PROMPT_HEADER).append("'+");} else {script.append(" prompt('").append(MSG_PROMPT_HEADER).append("'+");}// Begin JSONscript.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 JSONscript.append("]})");// End promptscript.append(");");// End functionscript.append(" }, ");}// End of objscript.append(" };");// End of if or elsescript.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 {@Overridepublic final void onProgressChanged(WebView view, int newProgress) {injectJavascriptInterfaces(view);super.onProgressChanged(view, newProgress);}@Overridepublic 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);}@Overridepublic final void onReceivedTitle(WebView view, String title) {injectJavascriptInterfaces(view);}}private class WebViewClientEx extends WebViewClient {@Overridepublic void onLoadResource(WebView view, String url) {injectJavascriptInterfaces(view);super.onLoadResource(view, url);}@Overridepublic void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {injectJavascriptInterfaces(view);super.doUpdateVisitedHistory(view, url, isReload);}@Overridepublic void onPageStarted(WebView view, String url, Bitmap favicon) {injectJavascriptInterfaces(view);super.onPageStarted(view, url, favicon);}@Overridepublic void onPageFinished(WebView view, String url) {injectJavascriptInterfaces(view);super.onPageFinished(view, url);}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 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 端,這樣就實現了一個完整的調用鏈。
引用
http://group.jobbole.com/26417/?utm_source=android.jobbole.com&utm_medium=sidebar-group-topic?
http://blog.csdn.net/jiangwei0910410003/article/details/52687530?
http://blog.csdn.net/leehong2005/article/details/11808557?
https://github.com/yushiwo/WebViewBugDemo/blob/master/src/com/lee/webviewbug/WebViewEx.java?
http://blog.csdn.net/sk719887916/article/details/52402470?
https://zhuanlan.zhihu.com/p/24202408?
https://github.com/lzyzsd/JsBridge?
http://www.jianshu.com/p/93cea79a2443#?
http://www.codexiu.cn/android/blog/33214/?
https://github.com/pedant/safe-java-js-webview-bridge?
http://blog.sina.com.cn/s/blog_777f9dbb0102v8by.html?
http://www.cnblogs.com/chaoyuehedy/p/5556557.html?
http://blogs.360.cn/360mobile/2014/09/22/webview%E8%B7%A8%E6%BA%90%E6%94%BB%E5%87%BB%E5%88%86%E6%9E%90/
https://my.oschina.net/zhibuji/blog/100580?
http://www.cnblogs.com/punkisnotdead/p/5062631.html?utm_source=tuicool&utm_medium=referral
總結
以上是生活随笔為你收集整理的android WebView详解,常见漏洞详解和安全源码(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android WebView详解,常见
- 下一篇: Android:你不知道的 WebVie