76

I have a link which will open in WebView. The problem is it cannot be open until I override onReceivedSslError like this:

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

I am getting security alert from 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.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.

If I remove onReceivedSslError (handler.proceed()), then page won't open.

Is there any way I can open the page in WebView and avoid security alert?

bluish
  • 26,356
  • 27
  • 122
  • 180
captaindroid
  • 2,868
  • 6
  • 31
  • 45
  • 2
    The idea is that you are supposed to examine the `SSLCertificate` inside the `SSLError` and determine if this is indeed a valid certificate for whatever server you are hitting. Then, and only then, do you call `proceed()`. Otherwise, you call `cancel()`. It would help if you could provide a [mcve], or at least a URL that triggers this callback. – CommonsWare Mar 21 '16 at 14:53
  • To correctly handle and check SslError check this answer: https://stackoverflow.com/a/49674821/1805520 – Yoda066 Jun 03 '20 at 17:16

8 Answers8

135

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.

As email said, onReceivedSslError should handle user is going to a page with invalid cert, such like a notify dialog. You should not proceed it directly.

For example, I add an alert dialog to make user have confirmed and seems Google no longer shows warning.


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

More explain about the email.

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

The email says the default implement ignored an important SSL security problem. So we need to handle it in our own app which used WebView. Notify user with a alert dialog is a simple way.

sakiM
  • 4,832
  • 5
  • 27
  • 33
  • I will update the code and will push to play store. Will let you know, Thanks. – captaindroid Mar 28 '16 at 07:51
  • 4
    The email does not actually say we should specify the user about anything, am I wrong? – oldergod Sep 09 '16 at 02:30
  • Can you please give more explanation that why is warning shows. – Aman Gupta - ΔMΔN Oct 27 '16 at 12:40
  • @sakiM : Is possible to not use `onReceivedSslError()` or not `Override code` inside? Is it better? – Huy Tower Dec 30 '16 at 03:32
  • @TranDucHuy the google warning indicate the inside WebView should notify handle the invalid certification problem. Maybe reject all these cases also can do it? – sakiM Dec 30 '16 at 05:00
  • Is it necessary to check case `onReceiveSslError`? Why we not ignored that case? I delete `onReceiveSslError` method without error also. – Huy Tower Dec 30 '16 at 06:00
  • 2
    IN my case, I implement this like @sakiM said. But google still rejected. Someone give me other solutions? – Bao HQ Jan 20 '17 at 02:14
  • Can't we simply call handler.cancel(); inside onReceivedSslError? Will google still reject it? – DevAndroid Feb 14 '17 at 10:10
  • 1
    @BaulHoa, by showing the above dialog works for me, please check your code carefully, there may be code yet to handle the `onReceiveSslError()`. Did you get the same email (as I got) from google again ? – captaindroid Apr 02 '17 at 14:11
  • 1
    final AlertDialog.Builder builder = new AlertDialog.Builder(this); it is not accepting "this" as parameter as i have copied this code to SystemWebViewClient.java(cordova app).So what I need to pass here? – nik Apr 12 '17 at 06:54
  • 1
    @sakiM I added the same code and Google rejected again with same reason. Any idea? – Vishesh Chandra Apr 24 '17 at 18:50
  • I got response from google support team that "vulnerable version of SslErrorHandler: com.backendless.SocialAsyncCallback$1;" so i update version of backendless library and issue solved...you can check more from https://stackoverflow.com/questions/35720753/android-google-play-warning-ssl-error-handler-vulnerability/44560218#44560218 – varotariya vajsi Jun 15 '17 at 06:36
  • 2
    Is there any way to solve this without a alert dialog, Is it possible to add certificate for web view from server. – livemaker Feb 28 '18 at 05:09
  • @livemaker I don't think , google will reject the app from playstore without confirmation alert dialog. I tried by submitting the app multiple times without alert dialog and google keep sending me the security alert. – captaindroid Mar 05 '18 at 13:04
  • 1
    I am facing same an issue any one got solution? – Parag Chauhan Feb 18 '19 at 05:48
  • 1
    is possible to solve the issue without alert message? – idumban Jul 16 '21 at 07:14
  • I implement this like @sakiM said. But google still rejected. Someone please help me? – Prosenjith Roy Dec 07 '21 at 07:42
11

The proposed solutions so far just bypass the security check, so they are not safe.

What I suggest is to embed the certificate(s) in the App, and when a SslError occurs, check that the server certificate matches one of the embedded certificates.

So here are the steps:

  1. Retrieve the certificate from the website.

    • Open the site on Safari
    • Click on the padlock icon near the website name
    • Click on Show Certificate
    • Drag and drop the certificate in a folder

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

  1. Copy the certificate (.cer file) into the res/raw folder of your app

  2. In your code, load the certificate(s) by calling 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. When a SslError occurs, check that the server certificate matches one embedded certificate. Note that it is not possible to directly compare certificates, so I use SslCertificate.saveState to put the certificate data into a Bundle, and then I compare all the bundle entries.

    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);
        }
    
    
    });
    
Arnaud SmartFun
  • 1,573
  • 16
  • 21
8

I needed to check our truststore before show any message to the user so I did this:

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();
    }
}
}
AmandaHLA
  • 119
  • 1
  • 2
7

Fix which works for me is just disable onReceivedSslError function defined in AuthorizationWebViewClient. In this case handler.cancel will be called in case of SSL error. However it works good with One Drive SSL certificates. Tested on Android 2.3.7, Android 5.1.

VK Da NINJA
  • 510
  • 7
  • 19
Naveen Prince P
  • 4,521
  • 3
  • 16
  • 17
  • Can't we simply call handler.cancel(); inside onReceivedSslError? Will google still reject it? – DevAndroid Feb 14 '17 at 11:34
  • @DevAndroid : Did it worked by simply calling handler.cancel(); inside onReceivedSslError ? – Sandip Lawate Apr 10 '17 at 06:09
  • But if we disable `onReceivedSslError ` method & remove `handler.proceed` then Webview is not able to load URL. What should be done in this case? I have asked this question here https://stackoverflow.com/q/65949051/8063842 BUT DIDN'T GET ANY SOLUTION – S.Ambika Apr 09 '21 at 11:38
6

According to Google Security Alert: Unsafe implementation of the interface X509TrustManager, Google Play won't support X509TrustManager from 11th July 2016:

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.

...

jww
  • 97,681
  • 90
  • 411
  • 885
  • It is important to to add the important part of linked articles. – David Gourde Jul 21 '16 at 09:08
  • 1
    `X509TrustManager` will still be supported. The issue is with developer's override of its methods, and doing things like `VERIFY_NONE` so there are no exceptions. Also see [The most dangerous code in the world: validating SSL certificates in non-browser software](https://crypto.stanford.edu/~dabo/pubs/abstracts/ssl-client-bugs.html). – jww Jul 21 '16 at 21:09
6

I had the same issue and tried all the above-mentioned suggestions as below.

  1. Implement onReceivedSslError() by giving the chance to the user to decide handler.proceed(); or handler.cancel(); when a SSL error occurred
  2. Implement onReceivedSslError() to call handler.cancel(); whenever a SSL issue occurred without considering user's decision.
  3. Implement onReceivedSslError() to verify SSL certificate locally addition to checking error.getPrimaryError() and providing the user to decide handler.proceed(); or handler.cancel(); only if the SSL certificate is valid. If not just call handler.cancel();
  4. Removing the implementation of onReceivedSslError() and just let to happen Android default behavior.

Even after trying all the above attempts, Google Play was keeping sending the same notification mail mentioning the same error and the old APK version (Even though in the all above attempts we changed both version code and version name in the Gradle)

We were in huge trouble and contacted Google Support via mail and asked

"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 ?"

After a few days, google support replied to our request as follows.

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.

The highlighted point was never found or mentioned in the play console or any forum.

According to that guideline in the classic Google play view, we checked the production track and there were both buggy versions and the latest bug fixed version but the bug fix version's rollout percentage is 20%. So, turned it to full rollout then the buggy version disappeared from the production track. After more than 24 hour review time version has come back.

NOTE: When we had this issue Google has just moved to a new UI version of their play console and it had missed some views in the previous UI version or classic view. Since we were using the latest view we couldn't notice what was happening. Simply what happened was Google reviewed the same previous buggy version of the APK since the new one was not full roll-out.

Udara Seneviratne
  • 2,303
  • 1
  • 33
  • 49
4

You can use SslError for show, some information about the error of this certificated, and you can write in your dialog the string of the type error.

@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);    }
Pabel
  • 652
  • 7
  • 15
  • Can't we simply call handler.cancel(); inside onReceivedSslError? Will google still reject it? – DevAndroid Feb 14 '17 at 11:34
  • @DevAndroid Google cancels that option because you do not inform the user. With this method filtering the error shows more information that because the connection has been rejected – Pabel Jun 30 '17 at 08:48
-2

In my situation:This error occured when we try to updated apk uploaded into the Google Play store,and getting SSL Error: Then i have used following code

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();
        }
    }
Soumen Das
  • 1,292
  • 17
  • 12