Веб-просмотр избегать предупреждений безопасности от Google Play при реализации onReceivedSslError

У меня есть ссылка, которая откроется в 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.stackru.com/questions и использовать теги "android-security" и "SslErrorHandler". Если вы используете стороннюю библиотеку, которая отвечает за это, пожалуйста, сообщите Сторонние и работать с ними для решения проблемы.

Чтобы подтвердить, что вы обновились правильно, загрузите обновленную версию в консоль разработчика и повторите попытку через пять часов. Если приложение не было правильно обновлено, мы выведем предупреждение.

Обратите внимание, что, хотя эти конкретные проблемы могут не затрагивать каждое приложение, использующее WebView SSL, лучше всегда быть в курсе всех обновлений безопасности. Приложения с уязвимостями, которые подвергают пользователей риску компрометации, могут считаться опасными продуктами в нарушение Политики в отношении контента и раздела 4.4 Соглашения о распространении для разработчиков.

Убедитесь, что все опубликованные приложения соответствуют Соглашению разработчика о распространении и Политике в отношении контента. Если у вас есть вопросы или проблемы, свяжитесь с нашей службой поддержки через Справочный центр разработчика Google Play.

Если я удалю onReceivedSslError (handler.proceed()), то страница не открывается.

Есть ли в любом случае, я могу открыть страницу в веб-обозрении и избежать предупреждения безопасности.

7 ответов

Решение

Чтобы правильно обрабатывать проверку сертификата 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. Уведомить пользователя с помощью диалогового окна с предупреждением - это простой способ.

Предлагаемые решения пока просто обходят проверку безопасности, поэтому они небезопасны.

Я предлагаю встроить сертификат (ы) в приложение, и при возникновении ошибки 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, а затем сравниваю все записи пакета.

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

Мне нужно было проверить наше хранилище доверенных сертификатов, прежде чем показывать какое-либо сообщение пользователю, поэтому я сделал это:

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

Исправление, которое работает для меня, это просто отключить onReceivedSslError функция, определенная в AuthorizationWebViewClient, В этом случае handler.cancel будет вызван в случае ошибки SSL. Однако он хорошо работает с SSL-сертификатами One Drive. Проверено на Android 2.3.7, Android 5.1.

Согласно Google Security Alert: небезопасная реализация интерфейса X509TrustManager, Google Play не будет поддерживать X509TrustManager с 11 июля 2016 года:

Здравствуйте, разработчик Google Play,

Ваши приложения, перечисленные в конце этого электронного письма, используют небезопасную реализацию интерфейса X509TrustManager. В частности, реализация игнорирует все ошибки проверки SSL-сертификата при установлении HTTPS-соединения с удаленным хостом, что делает ваше приложение уязвимым для атак "человек посередине". Злоумышленник может прочитать переданные данные (например, учетные данные для входа) и даже изменить данные, передаваемые по HTTPS-соединению. Если в вашей учетной записи более 20 уязвимых приложений, проверьте полный список в консоли разработчика.

Чтобы правильно обрабатывать проверку сертификата SSL, измените свой код в методе checkServerTrusted вашего пользовательского интерфейса X509TrustManager, чтобы повысить либо CertificateException, либо IllegalArgumentException, если сертификат, представленный сервером, не соответствует вашим ожиданиям. По техническим вопросам вы можете отправлять сообщения в Stack Overflow и использовать теги "android-security" и "TrustManager".

Пожалуйста, устраните эту проблему как можно скорее и увеличьте номер версии обновленного APK. Начиная с 17 мая 2016 года, Google Play будет блокировать публикацию любых новых приложений или обновлений, содержащих небезопасную реализацию интерфейса X509TrustManager.

Чтобы убедиться, что вы внесли правильные изменения, отправьте обновленную версию своего приложения на консоль разработчика и повторите проверку через пять часов. Если приложение не было правильно обновлено, мы выведем предупреждение.

Хотя эти конкретные проблемы могут не затрагивать каждое приложение с реализацией TrustManager, лучше не игнорировать ошибки проверки сертификата SSL. Приложения с уязвимостями, которые подвергают пользователей риску компрометации, могут считаться опасными продуктами в нарушение Политики в отношении контента и раздела 4.4 Соглашения о распространении для разработчиков.

...

У меня была такая же проблема, и я попробовал все вышеупомянутые предложения, как показано ниже.

  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 по почте и спросили

"Мы загружаем более поздние версии APK, но результат проверки показывает ту же ошибку, упоминающую старую версию APK с ошибками. В чем причина этого?"

Через несколько дней служба поддержки Google ответила на наш запрос следующим образом.

Обратите внимание, что вы должны полностью заменить версию 12 в своем производственном треке. Это означает, что вам нужно будет полностью развернуть более новую версию, чтобы деактивировать версию 12.

Выделенный пункт никогда не был найден и не упоминался ни на игровой консоли, ни на каком-либо форуме.

Согласно этому руководству в классическом представлении Google Play, мы проверили производственную версию, и там были как версии с ошибками, так и последняя версия с исправленными ошибками, но процент развертывания версии с исправлением ошибок составляет 20%. Итак, развернул его до полного развертывания, после чего версия с ошибками исчезла с производственной дорожки. После более чем 24-часового просмотра версия вернулась.

ПРИМЕЧАНИЕ. Когда у нас возникла эта проблема, Google только что перешел на новую версию пользовательского интерфейса своей игровой консоли, и в предыдущей версии пользовательского интерфейса или классическом представлении были пропущены некоторые представления. Поскольку мы использовали последнее представление, мы не могли заметить, что происходит. Просто произошло то, что Google проверил ту же предыдущую версию APK с ошибками, поскольку новая не была полностью развернута.

Вы можете использовать SslError для показа, некоторую информацию об ошибке этого сертифицированного, и вы можете написать в своем диалоге строку типа 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);    }

В моей ситуации: эта ошибка произошла, когда мы пытаемся обновить 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();
        }
    }
Другие вопросы по тегам