WebView:如何避免在实施onReceivedSslError时受到Google Play的安全警报?

76

我有一个链接,将在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”。如果你正在使用负责此问题的第三方库,请通知第三方并与他们合作解决该问题。
为确认你已正确升级,请将更新的版本上传到开发者控制台,并在五小时后返回检查。如果应用程序没有正确升级,我们将显示警告。
请注意,虽然这些特定问题可能不会影响每个使用WebView SSL的应用程序,但最好保持所有安全补丁的最新状态。具有暴露用户受到威胁的漏洞的应用程序可能被视为违反内容政策和开发人员分发协议第4.4节的危险产品。
请确保所有已发布的应用程序符合开发者分发协议和内容政策。如果你有问题或疑虑,请通过Google Play开发者帮助中心联系我们的支持团队。
如果我删除 onReceivedSslError (handler.proceed()),那么页面将无法打开。
有没有办法在 WebView 中打开页面并避免安全警报?

2
这个想法是,你应该检查SSLError中的SSLCertificate,并确定它是否确实是你正在访问的服务器的有效证书。然后,只有在这种情况下,你才调用proceed()。否则,你就调用cancel()。如果你能提供一个[mcve],或者至少提供一个触发此回调的URL,那将会很有帮助。 - CommonsWare
要正确处理和检查SslError,请参考此答案:https://dev59.com/IVoV5IYBdhLWcg3wmPtH#49674821 - Yoda066
8个回答

135

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

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

例如,我添加了一个警示对话框以确保用户已确认,似乎谷歌不再显示警告。


@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的应用程序中处理它。通过警报对话框通知用户是一种简单的方式。


4
这封电子邮件实际上并没有说我们应该具体说明任何事情给用户,我理解得对吗? - oldergod
2
在我的情况下,我像@sakiM所说的那样实现了这个。但是谷歌仍然拒绝了。有人能给我其他的解决方案吗? - Bao HQ
1
@BaulHoa,通过显示上述对话框,我可以确认它对我有效,请仔细检查您的代码,可能还有处理onReceiveSslError()的代码未完成。您是否收到了与我一样的来自Google的电子邮件? - captaindroid
1
最终的AlertDialog.Builder builder = new AlertDialog.Builder(this); 它不接受"this"作为参数,因为我已经将此代码复制到SystemWebViewClient.java(cordova应用程序)中。那么我需要传递什么? - nik
2
有没有不使用警告对话框的解决方法?是否可以从服务器为 Web 视图添加证书? - livemaker
显示剩余12条评论

11

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

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

以下是步骤:

  1. 从网站检索证书。

    • 在Safari上打开该网站
    • 单击网站名称旁边的小锁图标
    • 单击“显示证书”
    • 将证书拖放到文件夹中

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

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

  2. 在代码中通过调用loadSSLCertificates()方法加载证书(s)

    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条目。

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

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

TrustManagerUtil是什么?如何实现它? - ShriKant A
Similar:https://gist.github.com/iammert/a61042c45ee1d52c60ce7936ddc1e981#file-rawcertificatepinner-java-L57 - AmandaHLA

7

我经过测试发现,在AuthorizationWebViewClient中禁用onReceivedSslError函数可以解决问题。这样,在SSL错误的情况下,将会调用handler.cancel。但是对于One Drive SSL证书,则不需要进行此操作。此方法在Android 2.3.7和Android 5.1上测试通过。


我们能否在onReceivedSslError中简单地调用handler.cancel();?谷歌会拒绝吗? - DevAndroid
@DevAndroid:在onReceivedSslError中仅调用handler.cancel();就可以工作了吗? - Sandip Lawate
但是,如果我们禁用 onReceivedSslError 方法并删除 handler.proceed,那么 Webview 就无法加载 URL。在这种情况下应该怎么办?我已经在 https://stackoverflow.com/q/65949051/8063842 上提出了这个问题,但没有得到任何解决方案。 - S.Ambika

6
根据Google安全警报:X509TrustManager接口实现不安全,从2016年7月11日起,Google Play将不再支持X509TrustManager
你好,Google Play开发者,
本邮件末尾列出的应用程序使用了不安全的X509TrustManager接口实现。具体来说,当与远程主机建立HTTPS连接时,该实现忽略了所有SSL证书验证错误,从而使您的应用程序容易受到中间人攻击。攻击者可以读取传输的数据(例如登录凭据)甚至更改在HTTPS连接上传输的数据。如果您的帐户中有超过20个受影响的应用程序,请检查开发者控制台以获取完整列表。
为了正确处理SSL证书验证,请在自定义X509TrustManager接口的checkServerTrusted方法中更改代码,以便在服务器提供的证书不符合您的期望时引发CertificateException或IllegalArgumentException异常。对于技术问题,您可以发布到Stack Overflow并使用“android-security”和“TrustManager”标签。
请尽快解决此问题并增加升级APK的版本号。从2016年5月17日开始,Google Play将阻止发布任何包含不安全的X509TrustManager接口实现的新应用程序或更新。
为确认您已进行正确的更改,请将更新后的应用程序版本提交到开发者控制台,并在五个小时后再次查看。如果应用程序没有正确升级,我们将显示警告。
虽然这些特定问题可能不会影响每个使用TrustManager实现的应用程序,但最好不要忽略SSL证书验证错误。具有暴露用户面临危险的漏洞的应用程序可能被视为违反内容政策和开发者分发协议第4.4节的危险产品。

重要的是添加链接文章的重要部分。 - David Gourde
1
X509TrustManager 仍将得到支持。问题在于开发人员覆盖其方法并执行诸如 VERIFY_NONE 等操作,以避免出现异常。另请参见世界上最危险的代码:在非浏览器软件中验证 SSL 证书 - jww

6

我遇到了同样的问题,并尝试了以下所有建议:

  1. 在onReceivedSslError()中实现,当出现SSL错误时,让用户决定handler.proceed();或handler.cancel();
  2. 在onReceivedSslError()中实现,无论用户是否同意,都调用handler.cancel();以解决SSL问题。
  3. 在onReceivedSslError()中实现,在检查error.getPrimaryError()并提供用户决定handler.proceed();或handler.cancel();只有当SSL证书有效时才进行本地SSL证书验证。如果无效,只需调用handler.cancel();
  4. 删除onReceivedSslError()的实现,让Android使用默认行为。

即使尝试了以上所有方法,Google Play仍然不断发送相同的通知邮件,提到相同的错误和旧版APK版本(即使在上述所有尝试中我们更改了Gradle中的版本代码和版本名称)。

我们遇到了巨大的麻烦,通过电子邮件联系了Google支持,询问如下:

"我们正在上传更高版本的APK,但审核结果仍然显示相同的错误,并提到旧的有缺陷的APK版本。原因是什么?"

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

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

上述问题从未在Play控制台或任何论坛中找到或提及。

根据该指南,在经典的Google Play视图中,我们检查了生产线轨迹,并且既有有缺陷的版本也有最新的已修复漏洞版本,但是漏洞修复版本的推出百分比为20%。因此,将其全面推出,然后有缺陷的版本就从生产轨道中消失了。超过24小时的审核时间后,新版本回来了。

注意:当遇到此问题时,Google刚刚转移到其播放控制台的新UI版本,并且在以前的UI版本或经典视图中错过了一些视图。由于我们使用的是最新视图,因此无法注意到发生了什么。简单地说,由于新版本没有全面推出,因此Google重新审查了同一先前有缺陷的APK版本。


嘿,最终你采取了上面哪个建议?请告诉我一下,我也遇到了这个问题。 - Shashank singh
@Shashanksingh,我们已经全面升级到最新版本了。 - Udara Seneviratne
我理解您的意思了,您是想问在onReceivedSslError方法中,您是删除了它还是用handler.cancel()实现了它? - Shashank singh
从你在答案中提到的4种方法中,你是用哪一种进行的?我需要帮助。 - Shashank singh
1
@Shashanksingh,你可以采用第四种方法,如果没有必要,就不需要覆盖onReceivedSslError()。让它按照Android的默认行为发生即可。 - Udara Seneviratne
感谢 @Udara Seneviratne - Shashank singh

4

您可以使用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);    }

我们不能在onReceivedSslError内部简单地调用handler.cancel()吗?Google仍然会拒绝它吗? - DevAndroid
@DevAndroid 谷歌取消了该选项,因为您没有通知用户。 使用此方法过滤错误会显示更多信息,因为连接已被拒绝。 - Pabel

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接