Webview 在执行 onReceivedSslError 时避免来自 google 的安全警报

Webview avoid security alert from google play upon implementation of onReceivedSslError

我有一个 link 将在网络视图中打开。问题是它无法打开,直到我像这样覆盖 onReceivedSslError:

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

我收到来自 google 的安全警报 play saying:

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.whosebug.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中打开页面并避免安全警报。

对我有用的修复只是禁用 AuthorizationWebViewClient 中定义的 onReceivedSslError 函数。在这种情况下,如果出现 SSL 错误,将调用 handler.cancel。但是,它适用于 One Drive SSL 证书。在 Android 2.3.7、Android 5.1.

上测试

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 不再显示警告。


@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来显示,一些关于这个证书错误的信息,你可以在你的对话框中写上错误类型的字符串。

@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);    }

根据 Google Security Alert: Unsafe implementation of the interface 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.

...

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

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. 从网站检索证书。

    • 在 Safari 上打开网站
    • 单击网站名称附近的挂锁图标
    • 点击显示证书
    • 将证书拖放到文件夹中

https://www.markbrilman.nl/2012/03/howto-save-a-certificate-via-safari-on-mac/

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

  2. 在您的代码中,通过调用 loadSSLCertificates()

    加载证书
    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();
        }
    }
    
  3. 发生 SslError 时,检查服务器证书是否与一个嵌入式证书匹配。注意不能直接比较证书,所以我使用SslCertificate.saveState将证书数据放入一个Bundle,然后我比较所有的bundle条目。

    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);
        }
    
    
    });
    

在我的 situation:This 中,当我们尝试更新上传的 apk 时发生错误 进入 Google Play 商店,出现 SSL 错误: 然后我使用了以下代码

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();
        }
    }

我遇到了同样的问题并尝试了以下所有 above-mentioned 建议。

  1. 通过让用户有机会执行 onReceivedSslError() 决定 handler.proceed();或 handler.cancel();当 SSL 错误时 发生
  2. 实现 onReceivedSslError() 调用 handler.cancel();每当 没有考虑用户的决定就发生了 SSL 问题。
  3. 实现 onReceivedSslError() 以在本地验证 SSL 证书 除了检查 error.getPrimaryError() 并向用户提供 决定 handler.proceed();或 handler.cancel();仅当 SSL 证书有效。如果不只是调用 handler.cancel();
  4. 删除 onReceivedSslError() 的实现,只让 发生 Android 默认行为。

即使在尝试了上述所有尝试之后,Google Play 仍会继续发送相同的通知邮件,提及相同的错误和旧的 APK 版本(即使在上述所有尝试中我们都更改了版本代码和版本Gradle)

中的名称

我们遇到了很大的麻烦,通过邮件联系了 Google 支持并询问

"We are uploading the higher versions of the APK but the review result says the same error mentioning the old buggy APK version. What's the reason for that ?"

几天后,google支持回复我们的请求如下。

Please note that you must completely replace version 12 in your Production track. It means that you'll have to full rollout a higher version in order to deactivate version 12.

突出显示的点从未在游戏控制台或任何论坛中找到或提及。

根据经典 Google 播放视图中的指南,我们检查了生产轨道,同时存在错误版本和最新的错误修复版本,但错误修复版本的推出百分比为 20%。因此,将其全面推出,然后有缺陷的版本从生产轨道上消失了。经过24个多小时的审核时间版本回来了。

注意:当我们遇到这个问题时 Google 刚刚转​​移到新的 UI 版本的他们的游戏控制台并且它在以前的 UI 版本或经典视图。由于我们使用的是最新视图,因此我们无法注意到发生了什么。只是发生的事情是 Google 审查了 APK 的相同的先前错误版本,因为新版本不完整 roll-out。