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.
如果删除
无论如何,我可以在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.
正如电子邮件所说,
例如,我添加了一个警报对话框,以使用户已经确认,并且似乎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); } }); |
对我有用的解决方案是禁用
根据Google安全警报:X509TrustManager接口的不安全实现,自2016年7月11日起,Google Play将不支持
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(); } } |