Webview与js交互漏洞及解决方法(有注释代码)

来源:互联网 发布:基金公司招聘条件 知乎 编辑:程序博客网 时间:2024/04/30 15:36

前两天去Tencent面试,被面试官问到这个问题,答得不是很好,于是回来总结一下,并提供了解决方法与大家分享。

在Android中,netive与js交互已经不是什么新鲜事。大多数人都知道WebView存在一个漏洞,大致是因为js可以通过webview的window对象获得Class然后通过遍历所有的方法,找到runtime方法,边,虽然该漏洞已经在Android 4.2上修复了,即使用@JavascriptInterface代替addJavascriptInterface,但是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,所以我们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。

我们发现当setWebChromeClient之后,前端调用的一些js方法如:alert、Prompt、confirm等方法都会走你自定义的WebChromeClient类中相对应的重写的方法,由于alert、confirm使用频率较高,我们不建议处理,而prompt使用频率就少多了,甚至基本没使用,于是思路来了:我们可以让前端调用prompt这个方法,传过来一些信息,然后原生拦截prompt方法,获取这些信息,然后确定需要调用的jsbridge。

代码实现:

首先看一下目录结构:
这里写图片描述

前端部分

1、在页面加载时先定义好一些方法,包括生成一段前端与原生自己定义好的内容格式(URI)、调用prompt的方法;

//JSBridge.js//定义的URI内容格式如下://JSBridge://WindowJSBridge:821021544/toast?{"msg":"Hello JSBridge"}//JSBridge:固定标识,两端统一即可;//WindowJSBridge:要调用原生的bridge类名;//821021544:存储在前端callback数组中的index,代表是哪个jsbridge请求的callback;//toast:要调用的原生方法名;//{"msg":"Hello JSBridge"}:携带给原生的参数;(function (win) {    var hasOwnProperty = Object.prototype.hasOwnProperty;    var JSBridge = win.JSBridge || (win.JSBridge = {});    var JSBRIDGE_PROTOCOL = 'JSBridge';    var Inner = {        callbacks: {},        call: function (obj, method, params, callback) {            console.log(obj+" "+method+" "+params+" "+callback);            var port = Util.getPort();            console.log(port);            this.callbacks[port] = callback;            var uri=Util.getUri(obj,method,params,port);            console.log(uri);            window.prompt(uri, "");        },        onFinish: function (port, jsonObj){            var callback = this.callbacks[port];            callback && callback(jsonObj);            delete this.callbacks[port];        },    };    var Util = {        getPort: function () {            return Math.floor(Math.random() * (1 << 30));        },        getUri:function(obj, method, params, port){            params = this.getParam(params);            var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;            return uri;        },        getParam:function(obj){            if (obj && typeof obj === 'object') {                return JSON.stringify(obj);            } else {                return obj || '';            }        }    };    for (var key in Inner) {        if (!hasOwnProperty.call(JSBridge, key)) {            JSBridge[key] = Inner[key];        }    }})(window);

前端页面主要是通过点击button使用在上面js文件中定义好的方法调用原生方法。

<!DOCTYPE HTML , 前端页面><html><head>    <meta charset="utf-8">    <title>JSBridge</title>    <meta name="viewport"          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>    <script src="file:///android_asset/JSBridge.js" type="text/javascript"></script>    <script type="text/javascript">    </script>    <style>    </style></head><body><div>    <h3>JSBridge 测试</h3></div><ul class="list">    <li>        <div>            <button onclick="JSBridge.call('WindowJSBridge','toast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">                测试showToast            </button>        </div>    </li>    <br/></ul><ul class="list">    <li>        <div>            <button onclick="JSBridge.call('AppInfoJSBridge','getPackageName',{},function(res){alert(JSON.stringify(res))})">                测试子线程回调            </button>        </div>    </li>    <br/></ul></body></html>

原生代码:

1、自定义WebChromeClient,处理相对应方法

/** * @author caoyujie * WebChromeClient主要辅助WebView处理JavaScript的对话框、网站图标、网站title、加载进度等 */public class JSBridgeWebChromeClient extends WebChromeClient {    /**     * @param view     * @param url     * @param message       前端设置的内容     * @param defaultValue     * @param result        按确定时回调的结果, result.cancel()关闭回调,否则将卡住界面     * @return              true:交给客户端自定义处理   false:游览器默认处理     */    @Override    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {        JSBridgeManager.getInstance().excuteJS(view,message);        result.cancel();        return true;    }}

2、之前我们定义好的uri格式的信息就是message,我们生成一个JSBridge管理类,解析这个uri,得到需要调用的类和方法,处理相对应的前端请求:

/** * jsBridge处理类,采用单例 */public class JSBridgeManager {    private static JSBridgeManager INSTANCE;    private static String BRIDGE_PREFIX = "JSBridge";       //约定前缀    private static HashMap<String,HashMap<String,Method>> exposedMethods = new HashMap<>();    public static JSBridgeManager getInstance() {        if (INSTANCE == null) {            synchronized (JSBridgeManager.class) {                if (INSTANCE == null) {                    INSTANCE = new JSBridgeManager();                }            }        }        return INSTANCE;    }    /**     * 执行js所调用的原生代码     * @param webview     * @param uriString     前端传来的信息,包括了js类名、方法名、参数、callback等     */    public void excuteJS(WebView webview , String uriString) {        JSBridgeRequest jsBridgeRequest = parseJSBridge(uriString);        if (jsBridgeRequest == null)            return;        Class registJSBridge = null;        switch (jsBridgeRequest.getMethodName()) {            case "toast":                registJSBridge = WindowJSBridge.class;                break;            case "getPackageName":                registJSBridge = AppInfoJSBridge.class;                break;        }        if(registJSBridge != null){            regitstJSBridge(registJSBridge);                callJava(webview , registJSBridge , jsBridgeRequest);        }    }    /**     * 将前端传过来的信息转换成 js请求model     */    private JSBridgeRequest parseJSBridge(String uriString) {        JSBridgeRequest request = null;        try {            if (!TextUtils.isEmpty(uriString) && uriString.startsWith(BRIDGE_PREFIX)) {                request = new JSBridgeRequest();                Uri jsUri = Uri.parse(uriString);                request.setClassName(jsUri.getHost());                request.setPort(String.valueOf(jsUri.getPort()));                request.setParm(new JSONObject(jsUri.getQuery()));                String path = jsUri.getPath();                if (!TextUtils.isEmpty(path)) {                    request.setMethodName(path.replace("/", ""));                }            }        } catch (Exception e) {            e.printStackTrace();        }        return request;    }    /**     * 根据前端指定的jsbridge类名找到该类,并获得其提供的方法     * @param clazz     */    private void regitstJSBridge(Class<IBridge> clazz){        String exposedName = clazz.getSimpleName();        if(!exposedMethods.containsKey(exposedName)){            try {                exposedMethods.put(exposedName, getAllMethod(clazz));            } catch (Exception e){                e.printStackTrace();            }        }    }    /**     * 获得某个jsbridge类中所有公开方法     * @param clazz   具体的jsbridge类     */    private HashMap<String,Method> getAllMethod(Class<IBridge> clazz){        HashMap<String,Method> methods = new HashMap<>();        Method[] declaredMethods = clazz.getDeclaredMethods();        for (Method method : declaredMethods) {            String name;            if(method.getModifiers() != Modifier.PUBLIC || (name = method.getName()) == null){                continue;            }            Class[] parameters = method.getParameterTypes();            if(parameters != null && parameters.length == 3){                if(parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == JsCallback.class){                    methods.put(name , method);                }            }        }        return methods;    }    /**     * 通过反射得到该类的实例,并执行前端需要的js方法     * @param webview     * @param classImpl    jsbridge类的class对象     * @param request      封装的js请求model     */    private void callJava(WebView webview , Class<IBridge> classImpl , JSBridgeRequest request){        if(request == null)            return;        String className = request.getClassName();        if (exposedMethods.containsKey(request.getClassName())) {            HashMap<String, Method> methods = exposedMethods.get(className);            String methodName = request.getMethodName();            if (methods != null && methods.size() > 0 && methods.containsKey(methodName)) {                Method method = methods.get(methodName);                try {                    method.invoke(classImpl.newInstance(), webview, request.getParm(), new JsCallback(webview, request.getPort()));                } catch (Exception e) {                    e.printStackTrace();                }            }        }    }    /**     * 释放资源     * 游览器页面关闭时调用     */    public void release(){        exposedMethods.clear();    }}

另外一些相关类

/** * Created by caoyujie on 17/3/13. * 根据前端请求信息转换成的请求实体类 */public class JSBridgeRequest {    /**     * native方法名     */    private String methodName;    /**     * jsbridge的类名     */    private String className;    /**     * 请求参数     */    private JSONObject parm;    /**     * 区分回调的标志     */    private String port;    public String getMethodName() {        return methodName;    }    public void setMethodName(String methodName) {        this.methodName = methodName;    }    public String getClassName() {        return className;    }    public void setClassName(String className) {        this.className = className;    }    public JSONObject getParm() {        return parm;    }    public void setParm(JSONObject parm) {        this.parm = parm;    }    public String getPort() {        return port;    }    public void setPort(String port) {        this.port = port;    }}
/** * Created by caoyujie on 17/3/13. * 返回结果给前端的回调类 */public class JsCallback {    private Handler mHandler = new Handler(Looper.getMainLooper());    private static final String CALLBACK_FORMAT = "javascript:JSBridge.onFinish(%s,%s);";    private String mPort;    private WeakReference<WebView> webview;    public JsCallback(WebView view, String port) {        mPort = port;        webview = new WeakReference<WebView>(view);    }    /**     * 添加回调     * @param callbackEntity   回调给前端结果实体类     */    public void apply(JsCallback.Entity callbackEntity) {        JSONObject jsonObject = callbackEntity.toJSON();        final String execJs = String.format(CALLBACK_FORMAT, mPort, String.valueOf(jsonObject));        if (webview != null && webview.get() != null) {            mHandler.post(new Runnable() {                @Override                public void run() {                    webview.get().loadUrl(execJs);                }            });        }    }    /**     * 回调给前端结果实体类     */    public static class Entity {        /**         * 成功码         */        private int code;        /**         * 响应信息         */        private String message;        /**         * 回调数据         */        private String data;        public int getCode() {            return code;        }        public void setCode(int code) {            this.code = code;        }        public String getMessage() {            return message;        }        public void setMessage(String message) {            this.message = message;        }        public String getData() {            return data;        }        public void setData(String data) {            this.data = data;        }        public JSONObject toJSON() {            JSONObject jsonObject = null;            try {                jsonObject = new JSONObject();                jsonObject.put("code", code);                jsonObject.put("message", message);                jsonObject.put("data", data);            } catch (JSONException e) {                e.printStackTrace();            }            return jsonObject;        }    }}
/** * Created by caoyujie  * jsbridge基类,写一些公用方法 */public interface IBridge {}
/** * Created by caoyujie on 17/3/13. * 窗口类js接口 */public class WindowJSBridge implements IBridge {    public void toast(WebView webView , JSONObject parmObject , JsCallback callback){        Toast.makeText( webView.getContext() , parmObject.optString("msg"), Toast.LENGTH_SHORT).show();    }}/** * Created by caoyujie on 17/3/13. * 获取应用信息类js接口 */public class AppInfoJSBridge implements IBridge {    public void getPackageName(WebView webView , JSONObject parmObject , JsCallback callback){        JsCallback.Entity entity = new JsCallback.Entity();        entity.setCode(200);        entity.setMessage("成功");        entity.setData(webView.getContext().getPackageName());        callback.apply(entity);    }}

查看源码: github源码地址

最后上一张效果演示:
这里写图片描述

0 0