70

我有一个将在 webview 中打开的链接。问题是它无法打开,直到我像这样覆盖 onReceivedSslError:

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

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

安全警报 您的应用程序的 WebViewClient.onReceivedSslError 处理程序的实现不安全。具体来说,该实现会忽略所有 SSL 证书验证错误,从而使您的应用容易受到中间人攻击。攻击者可以更改受影响的 WebView 的内容,读取传输的数据(例如登录凭据),并使用 JavaScript 在应用程序内执行代码。

要正确处理 SSL 证书验证,请更改您的代码以在服务器提供的证书符合您的期望时调用 SslErrorHandler.proceed(),否则调用 SslErrorHandler.cancel()。包含受影响应用程序和类的电子邮件警报已发送到您的开发者帐户地址。

请尽快解决此漏洞,并增加升级后的APK版本号。有关 SSL 错误处理程序的更多信息,请参阅开发者帮助中心中的文档。对于其他技术问题,您可以发布到https://www.stackoverflow.com/questions并使用标签“android-security”和“SslErrorHandler”。如果您使用的是负责此问题的第 3 方库,请通知第 3 方并与他们一起解决问题。

要确认您已正确升级,请将更新后的版本上传到开发者控制台,并在五小时后回来查看。如果应用程序未正确升级,我们将显示警告。

请注意,虽然这些特定问题可能不会影响使用 WebView SSL 的每个应用程序,但最好及时更新所有安全补丁。存在使用户面临泄露风险的漏洞的应用程序可能会被视为违反内容政策和开发者分发协议第 4.4 节的危险产品。

请确保所有发布的应用都符合开发者分发协议和内容政策。如果您有任何问题或疑虑,请通过 Google Play 开发者帮助中心联系我们的支持团队。

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

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

4

8 回答 8

129

要正确处理 SSL 证书验证,请更改您的代码以在服务器提供的证书符合您的期望时调用 SslErrorHandler.proceed(),否则调用 SslErrorHandler.cancel()。

正如电子邮件所说,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();
}

更多关于电子邮件的解释。

具体来说,该实现会忽略所有 SSL 证书验证错误,从而使您的应用容易受到中间人攻击。

电子邮件称默认工具忽略了一个重要的 SSL 安全问题。所以我们需要在我们自己的使用 WebView 的应用程序中处理它。使用警告对话框通知用户是一种简单的方法。

于 2016-03-22T06:53:03.367 回答
9

到目前为止,建议的解决方案只是绕过安全检查,因此它们并不安全。

我建议将证书嵌入应用程序中,当发生 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);
        }
    
    
    });
    
于 2018-04-05T14:24:50.257 回答
8

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

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();
    }
}
}
于 2017-10-18T14:19:33.330 回答
7

onReceivedSslError对我有用的修复只是禁用AuthorizationWebViewClient. 在这种情况下handler.cancel,将在 SSL 错误的情况下调用。但是它适用于 One Drive SSL 证书。在 Android 2.3.7、Android 5.1 上测试。

于 2016-03-21T13:09:13.680 回答
6

根据Google Security Alert: Unsafe implementation of the interface X509TrustManager,Google Play 从 2016 年 7 月 11 日起将不再支持X509TrustManager

您好 Google Play 开发者,

此电子邮件末尾列出的您的应用程序使用了 X509TrustManager 接口的不安全实现。具体来说,在与远程主机建立 HTTPS 连接时,实现会忽略所有 SSL 证书验证错误,从而使您的应用容易受到中间人攻击。攻击者可以读取传输的数据(例如登录凭据),甚至更改通过 HTTPS 连接传输的数据。如果您的帐户中有超过 20 个受影响的应用,请查看开发者控制台以获取完整列表。

要正确处理 SSL 证书验证,请更改自定义 X509TrustManager 接口的 checkServerTrusted 方法中的代码,以在服务器提供的证书不符合您的期望时引发 CertificateException 或 IllegalArgumentException。对于技术问题,您可以发布到 Stack Overflow 并使用标签“android-security”和“TrustManager”。</p>

请尽快解决此问题,并增加升级后的 APK 的版本号。从 2016 年 5 月 17 日开始,Google Play 将阻止发布任何包含 X509TrustManager 接口的不安全实现的新应用或更新。

要确认您所做的更改是否正确,请将应用的更新版本提交到开发者控制台,并在五小时后回来查看。如果应用程序未正确升级,我们将显示警告。

虽然这些特定问题可能不会影响使用 TrustManager 实现的每个应用程序,但最好不要忽略 SSL 证书验证错误。存在使用户面临泄露风险的漏洞的应用程序可能会被视为违反内容政策和开发者分发协议第 4.4 节的危险产品。

...

于 2016-07-21T08:42:05.723 回答
4

我遇到了同样的问题,并尝试了上述所有建议,如下所示。

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

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

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

“我们正在上传更高版本的 APK,但审核结果显示同样的错误,提到旧的错误 APK 版本。这是什么原因?”

几天后,谷歌支持回复了我们的请求,如下所示。

请注意,您必须在生产轨道中完全替换版本 12。这意味着您必须完全推出更高版本才能停用版本 12

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

根据经典谷歌播放视图中的该指南,我们检查了生产轨道,既有错误版本,也有最新的错误修复版本,但错误修复版本的推出百分比为 20%。因此,将其转为全面推出,然后错误版本从生产轨道上消失了。经过超过 24 小时的审核时间版本又回来了。

注意:当我们遇到这个问题时,谷歌刚刚转移到他们的游戏控制台的新 UI 版本,它错过了以前 UI 版本或经典视图中的一些视图。由于我们使用的是最新视图,因此我们无法注意到发生了什么。发生的事情很简单,因为新版本还没有完全推出,所以谷歌审查了之前有缺陷的 APK 版本。

于 2020-09-10T11:43:35.763 回答
3

您可以使用 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);    }
于 2016-07-19T08:03:15.200 回答
-2

在我的情况下:当我们尝试更新上传到 Google Play 商店的 apk 并出现 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();
        }
    }
于 2018-06-01T11:47:45.970 回答