关于android:Webview避免在onReceivedSslError实现时出现来自Google Play的安全警报

Webview avoid security alert from google play upon implementation of onReceivedSslError

我有一个链接会在webview中打开。 问题是,直到我像这样覆盖onReceivedSslError,它才能打开:

1
2
3
4
 @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            handler.proceed();
        }

我从Google Play收到安全警报,说:

Security alert
Your application has an unsafe implementation of the WebViewClient.onReceivedSslError handler. Specifically, the implementation ignores all SSL certificate validation errors, making your app vulnerable to man-in-the-middle attacks. An attacker could change the affected WebView's content, read transmitted data (such as login credentials), and execute code inside the app using JavaScript.

To properly handle SSL certificate validation, change your code to invoke SslErrorHandler.proceed() whenever the certificate presented by the server meets your expectations, and invoke SslErrorHandler.cancel() otherwise. An email alert containing the affected app(s) and class(es) has been sent to your developer account address.

Please address this vulnerability as soon as possible and increment the version number of the upgraded APK. For more information about the SSL error handler, please see our documentation in the Developer Help Center. For other technical questions, you can post to https://www.stackoverflow.com/questions and use the tags"android-security" and"SslErrorHandler." If you are using a 3rd party library that’s responsible for this, please notify the 3rd party and work with them to address the issue.

To confirm that you've upgraded correctly, upload the updated version to the Developer Console and check back after five hours. If the app hasn't been correctly upgraded, we will display a warning.

Please note, while these specific issues may not affect every app that uses WebView SSL, it's best to stay up to date on all security patches. Apps with vulnerabilities that expose users to risk of compromise may be considered dangerous products in violation of the Content Policy and section 4.4 of the Developer Distribution Agreement.

Please ensure all apps published are compliant with the Developer Distribution Agreement and Content Policy. If you have questions or concerns, please contact our support team through the Google Play Developer Help Center.

如果删除onReceivedSslError (handler.proceed()),则页面将无法打开。

无论如何,我可以在webview中打开页面并避免安全警报。


To properly handle SSL certificate validation, change your code to
invoke SslErrorHandler.proceed() whenever the certificate presented by
the server meets your expectations, and invoke
SslErrorHandler.cancel() otherwise.

正如电子邮件所说,onReceivedSslError应该处理用户转到带有无效证书的页面,例如通知对话框。您不应该直接进行此操作。

例如,我添加了一个警报对话框,以使用户已经确认,并且似乎Google不再显示警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
    final AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage(R.string.notification_error_ssl_cert_invalid);
    builder.setPositiveButton("continue", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            handler.proceed();
        }
    });
    builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            handler.cancel();
        }
    });
    final AlertDialog dialog = builder.create();
    dialog.show();
}

有关电子邮件的更多说明。

Specifically, the implementation ignores all SSL certificate validation
errors, making your app vulnerable to man-in-the-middle attacks.

电子邮件中说默认工具忽略了一个重要的SSL安全问题。因此,我们需要在使用WebView的自己的应用程序中对其进行处理。通过警报对话框通知用户是一种简单的方法。


到目前为止,所提出的解决方案只是绕过安全检查,因此不安全。

我建议将证书嵌入到应用程序中,并在发生SslError时,检查服务器证书是否与嵌入式证书之一匹配。

所以这是步骤:

  • 从网站上获取证书。

    • 在Safari上打开网站
    • 单击网站名称附近的挂锁图标
    • 点击显示证书
    • 将证书拖放到文件夹中
  • 参见https://www.markbrilman.nl/2012/03/howto-save-a-certificate-via-safari-on-mac/

  • 将证书(.cer文件)复制到应用程序的res / raw文件夹中

  • 在您的代码中,通过调用loadSSLCertificates()来加载证书。

    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
    private static final int[] CERTIFICATES = {
            R.raw.my_certificate,   // you can put several certificates
    };
    private ArrayList<SslCertificate> certificates = new ArrayList<>();

    private void loadSSLCertificates() {
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            for (int rawId : CERTIFICATES) {
                InputStream inputStream = getResources().openRawResource(rawId);
                InputStream certificateInput = new BufferedInputStream(inputStream);
                try {
                    Certificate certificate = certificateFactory.generateCertificate(certificateInput);
                    if (certificate instanceof X509Certificate) {
                        X509Certificate x509Certificate = (X509Certificate) certificate;
                        SslCertificate sslCertificate = new SslCertificate(x509Certificate);
                        certificates.add(sslCertificate);
                    } else {
                        Log.w(TAG,"Wrong Certificate format:" + rawId);
                    }
                } catch (CertificateException exception) {
                    Log.w(TAG,"Cannot read certificate:" + rawId);
                } finally {
                    try {
                        certificateInput.close();
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (CertificateException e) {
            e.printStackTrace();
        }
    }
  • 发生SslError时,请检查服务器证书是否与一个嵌入式证书匹配。请注意,不可能直接比较证书,因此我使用SslCertificate.saveState将证书数据放入捆绑包中,然后比较所有捆绑包条目。

    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
    webView.setWebViewClient(new WebViewClient() {

        @Override
        public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {

            // Checks Embedded certificates
            SslCertificate serverCertificate = error.getCertificate();
            Bundle serverBundle = SslCertificate.saveState(serverCertificate);
            for (SslCertificate appCertificate : certificates) {
                if (TextUtils.equals(serverCertificate.toString(), appCertificate.toString())) { // First fast check
                    Bundle appBundle = SslCertificate.saveState(appCertificate);
                    Set<String> keySet = appBundle.keySet();
                    boolean matches = true;
                    for (String key : keySet) {
                        Object serverObj = serverBundle.get(key);
                        Object appObj = appBundle.get(key);
                        if (serverObj instanceof byte[] && appObj instanceof byte[]) {     // key"x509-certificate"
                            if (!Arrays.equals((byte[]) serverObj, (byte[]) appObj)) {
                                matches = false;
                                break;
                            }
                        } else if ((serverObj != null) && !serverObj.equals(appObj)) {
                            matches = false;
                            break;
                        }
                    }
                    if (matches) {
                        handler.proceed();
                        return;
                    }
                }
            }

            handler.cancel();
            String message ="SSL Error" + error.getPrimaryError();
            Log.w(TAG, message);
        }


    });

  • 对我有用的解决方案是禁用AuthorizationWebViewClient中定义的onReceivedSslError功能。在这种情况下,如果发生SSL错误,将调用handler.cancel。但是,它可以与One Drive SSL证书一起使用。在Android 2.3.7和Android 5.1上进行了测试。


    根据Google安全警报:X509TrustManager接口的不安全实现,自2016年7月11日起,Google Play将不支持X509TrustManager

    Hello Google Play Developer,

    Your app(s) listed at the end of this email use an unsafe
    implementation of the interface X509TrustManager. Specifically, the
    implementation ignores all SSL certificate validation errors when
    establishing an HTTPS connection to a remote host, thereby making your
    app vulnerable to man-in-the-middle attacks. An attacker could read
    transmitted data (such as login credentials) and even change the data
    transmitted on the HTTPS connection. If you have more than 20 affected
    apps in your account, please check the Developer Console for a full
    list.

    To properly handle SSL certificate validation, change your code in the
    checkServerTrusted method of your custom X509TrustManager interface to
    raise either CertificateException or IllegalArgumentException whenever
    the certificate presented by the server does not meet your
    expectations. For technical questions, you can post to Stack Overflow
    and use the tags"android-security" and"TrustManager."

    Please address this issue as soon as possible and increment the
    version number of the upgraded APK. Beginning May 17, 2016, Google
    Play will block publishing of any new apps or updates containing the
    unsafe implementation of the interface X509TrustManager.

    To confirm you’ve made the correct changes, submit the updated version
    of your app to the Developer Console and check back after five hours.
    If the app hasn’t been correctly upgraded, we will display a warning.

    While these specific issues may not affect every app with the
    TrustManager implementation, it’s best not to ignore SSL certificate
    validation errors. Apps with vulnerabilities that expose users to risk
    of compromise may be considered dangerous products in violation of the
    Content Policy and section 4.4 of the Developer Distribution
    Agreement.

    ...


    在向用户显示任何消息之前,我需要检查我们的信任库,所以我这样做:

    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
    public class MyWebViewClient extends WebViewClient {
    private static final String TAG = MyWebViewClient.class.getCanonicalName();

    Resources resources;
    Context context;

    public MyWebViewClient(Resources resources, Context context){
        this.resources = resources;
        this.context = context;
    }

    @Override
    public void onReceivedSslError(WebView v, final SslErrorHandler handler, SslError er){
        // first check certificate with our truststore
        // if not trusted, show dialog to user
        // if trusted, proceed
        try {
            TrustManagerFactory tmf = TrustManagerUtil.getTrustManagerFactory(resources);

            for(TrustManager t: tmf.getTrustManagers()){
                if (t instanceof X509TrustManager) {

                    X509TrustManager trustManager = (X509TrustManager) t;

                    Bundle bundle = SslCertificate.saveState(er.getCertificate());
                    X509Certificate x509Certificate;
                    byte[] bytes = bundle.getByteArray("x509-certificate");
                    if (bytes == null) {
                        x509Certificate = null;
                    } else {
                        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
                        Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
                        x509Certificate = (X509Certificate) cert;
                    }
                    X509Certificate[] x509Certificates = new X509Certificate[1];
                    x509Certificates[0] = x509Certificate;

                    trustManager.checkServerTrusted(x509Certificates,"ECDH_RSA");
                }
            }
            Log.d(TAG,"Certificate from" + er.getUrl() +" is trusted.");
            handler.proceed();
        }catch(Exception e){
            Log.d(TAG,"Failed to access" + er.getUrl() +". Error:" + er.getPrimaryError());
            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
            String message ="SSL Certificate error.";
            switch (er.getPrimaryError()) {
                case SslError.SSL_UNTRUSTED:
                    message ="O certificado n?o é confiável.";
                    break;
                case SslError.SSL_EXPIRED:
                    message ="O certificado expirou.";
                    break;
                case SslError.SSL_IDMISMATCH:
                    message ="Hostname inválido para o certificado.";
                    break;
                case SslError.SSL_NOTYETVALID:
                    message ="O certificado é inválido.";
                    break;
            }
            message +=" Deseja continuar mesmo assim?";

            builder.setTitle("Erro");
            builder.setMessage(message);
            builder.setPositiveButton("Sim", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    handler.proceed();
                }
            });
            builder.setNegativeButton("N?o", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    handler.cancel();
                }
            });
            final AlertDialog dialog = builder.create();
            dialog.show();
        }
    }
    }

    您可以使用SslError进行显示,以及有关此证书错误的一些信息,还可以在对话框中输入错误类型的字符串。

    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
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        final SslErrorHandler handlerFinal;
        handlerFinal = handler;
        int mensaje ;
        switch(error.getPrimaryError()) {
            case SslError.SSL_DATE_INVALID:
                mensaje = R.string.notification_error_ssl_date_invalid;
                break;
            case SslError.SSL_EXPIRED:
                mensaje = R.string.notification_error_ssl_expired;
                break;
            case SslError.SSL_IDMISMATCH:
                mensaje = R.string.notification_error_ssl_idmismatch;
                break;
            case SslError.SSL_INVALID:
                mensaje = R.string.notification_error_ssl_invalid;
                break;
            case SslError.SSL_NOTYETVALID:
                mensaje = R.string.notification_error_ssl_not_yet_valid;
                break;
            case SslError.SSL_UNTRUSTED:
                mensaje = R.string.notification_error_ssl_untrusted;
                break;
            default:
                mensaje = R.string.notification_error_ssl_cert_invalid;
        }

        AppLogger.e("OnReceivedSslError handel.proceed()");

        View.OnClickListener acept = new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                dialog.dismiss();
                handlerFinal.proceed();
            }
        };

        View.OnClickListener cancel = new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                dialog.dismiss();
                handlerFinal.cancel();
            }
        };

        View.OnClickListener listeners[] = {cancel, acept};
        dialog = UiUtils.showDialog2Buttons(activity, R.string.info, mensaje, R.string.popup_custom_cancelar, R.string.popup_custom_cancelar, listeners);    }


    在我的情况下:当我们尝试更新上载的APK时发生此错误
    进入Google Play商店,并收到SSL错误:
    然后我用下面的代码

    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
    private class MyWebViewClient extends WebViewClient {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                try {
                    progressDialog.dismiss();
                } catch (WindowManager.BadTokenException e) {
                    e.printStackTrace();
                }
                super.onPageFinished(view, url);
            }

            @Override
            public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {

     final AlertDialog.Builder builder = new AlertDialog.Builder(PayNPayWebActivity.this);
                builder.setMessage(R.string.notification_error_ssl_cert_invalid);
                builder.setPositiveButton("continue", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        handler.proceed();
                    }
                });
                builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        handler.cancel();
                    }
                });
                final AlertDialog dialog = builder.create();
                dialog.show();
            }
        }