WebView: cómo evitar la alerta de seguridad de Google Play tras la implementación de onReceivedSslError

15 minutos de lectura

avatar de usuario
capitandroide

Tengo un enlace que se abrirá en WebView. El problema es que no se puede abrir hasta que anule onReceivedSslError como esto:

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

Recibo una alerta de seguridad de Google Play que dice:

Alerta de seguridad Su aplicación tiene una implementación no segura del controlador WebViewClient.onReceivedSslError. Específicamente, la implementación ignora todos los errores de validación de certificados SSL, lo que hace que su aplicación sea vulnerable a los ataques de intermediarios. Un atacante podría cambiar el contenido de WebView afectado, leer los datos transmitidos (como las credenciales de inicio de sesión) y ejecutar código dentro de la aplicación usando JavaScript.

Para manejar correctamente la validación del certificado SSL, cambie su código para invocar SslErrorHandler.proceed() siempre que el certificado presentado por el servidor cumpla con sus expectativas, e invoque SslErrorHandler.cancel() de lo contrario. Se envió una alerta por correo electrónico que contiene las aplicaciones y clases afectadas a la dirección de su cuenta de desarrollador.

Solucione esta vulnerabilidad lo antes posible e incremente el número de versión del APK actualizado. Para obtener más información sobre el controlador de errores SSL, consulte nuestra documentación en el Centro de ayuda para desarrolladores. Para otras preguntas técnicas, puede publicar en https://www.stackoverflow.com/questions y usar las etiquetas “android-security” y “SslErrorHandler”. Si está utilizando una biblioteca de terceros que es responsable de esto, notifique al tercero y trabaje con ellos para solucionar el problema.

Para confirmar que se actualizó correctamente, cargue la versión actualizada en Developer Console y vuelva a verificar después de cinco horas. Si la aplicación no se ha actualizado correctamente, mostraremos una advertencia.

Tenga en cuenta que, si bien es posible que estos problemas específicos no afecten a todas las aplicaciones que usan WebView SSL, es mejor mantenerse actualizado con todos los parches de seguridad. Las aplicaciones con vulnerabilidades que exponen a los usuarios al riesgo de compromiso pueden considerarse productos peligrosos en violación de la Política de contenido y la sección 4.4 del Acuerdo de distribución para desarrolladores.

Asegúrese de que todas las aplicaciones publicadas cumplan con el Acuerdo de distribución para desarrolladores y la Política de contenido. Si tiene preguntas o inquietudes, comuníquese con nuestro equipo de soporte a través del Centro de ayuda para desarrolladores de Google Play.

si elimino onReceivedSslError (handler.proceed())entonces la página no se abrirá.

¿Hay alguna manera de que pueda abrir la página en WebView y evitar la alerta de seguridad?

  • La idea es que se supone que debes examinar el SSLCertificate dentro de SSLError y determine si este es realmente un certificado válido para cualquier servidor que esté accediendo. Entonces, y solo entonces, llamas proceed(). De lo contrario, llama cancel(). Sería útil si pudiera proporcionar un ejemplo reproducible mínimo, o al menos una URL que active esta devolución de llamada.

    – CommonsWare

    21 de marzo de 2016 a las 14:53

  • Para manejar y verificar correctamente SslError, verifique esta respuesta: stackoverflow.com/a/49674821/1805520

    – Yoda066

    3 de junio de 2020 a las 17:16

avatar de usuario
sakiM

Para manejar correctamente la validación del certificado SSL, cambie su código para invocar SslErrorHandler.proceed() siempre que el certificado presentado por el servidor cumpla con sus expectativas, e invoque SslErrorHandler.cancel() de lo contrario.

Como dijo el correo electrónico, onReceivedSslError debe controlar que el usuario vaya a una página con un certificado no válido, como un cuadro de diálogo de notificación. No debe proceder directamente.

Por ejemplo, agrego un cuadro de diálogo de alerta para que el usuario haya confirmado y parece que Google ya no muestra una advertencia.


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

Más explicaciones sobre el correo electrónico.

Específicamente, la implementación ignora todos los errores de validación de certificados SSL, lo que hace que su aplicación sea vulnerable a los ataques de intermediarios.

El correo electrónico dice que la implementación predeterminada ignoró un importante problema de seguridad SSL. Entonces, debemos manejarlo en nuestra propia aplicación que usa WebView. Notificar al usuario con un cuadro de diálogo de alerta es una forma sencilla.

  • El correo electrónico en realidad no dice que debemos especificar al usuario sobre nada, ¿me equivoco?

    – dios mayor

    9 de septiembre de 2016 a las 2:30

  • EN mi caso, implemento esto como dijo @sakiM. Pero google todavía rechazó. Alguien me da otras soluciones?

    – Sede central de Bao

    20 de enero de 2017 a las 2:14


  • @BaulHoa, al mostrar el cuadro de diálogo anterior funciona para mí, verifique su código con cuidado, es posible que aún haya código para manejar el onReceiveSslError(). ¿Obtuviste el mismo correo electrónico (que recibí yo) de Google otra vez?

    – capitandroide

    2 de abril de 2017 a las 14:11

  • generador AlertDialog.Builder final = new AlertDialog.Builder(this); no está aceptando “esto” como parámetro, ya que copié este código en SystemWebViewClient.java (aplicación cordova). Entonces, ¿qué debo pasar aquí?

    – Nik

    12 de abril de 2017 a las 6:54


  • ¿Hay alguna forma de resolver esto sin un cuadro de diálogo de alerta? ¿Es posible agregar un certificado para la vista web desde el servidor?

    – creador de vida

    28 de febrero de 2018 a las 5:09

Las soluciones propuestas hasta ahora simplemente eluden el control de seguridad, por lo que no son seguras.

Lo que sugiero es incrustar los certificados en la aplicación y, cuando se produzca un SslError, compruebe que el certificado del servidor coincida con uno de los certificados incrustados.

Así que aquí están los pasos:

  1. Recupere el certificado del sitio web.

    • Abre el sitio en Safari
    • Haga clic en el ícono del candado cerca del nombre del sitio web
    • Haga clic en Mostrar certificado
    • Arrastre y suelte el certificado en una carpeta

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

  1. Copie el certificado (archivo .cer) en la carpeta res/raw de su aplicación

  2. En su código, cargue los certificados llamando a 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. Cuando se produce un SslError, compruebe que el certificado del servidor coincida con un certificado incrustado. Tenga en cuenta que no es posible comparar certificados directamente, por lo que uso SslCertificate.saveState para colocar los datos del certificado en un paquete y luego comparo todas las entradas del paquete.

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

Necesitaba verificar nuestro almacén de confianza antes de mostrar cualquier mensaje al usuario, así que hice esto:

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

  • ¿Qué es TrustManagerUtil? ¿Cómo implementarlo?

    – ShriKant A

    23 de febrero de 2021 a las 6:17

  • Similar:gist.github.com/iammert/…

    – Amanda HLA

    24 de febrero de 2021 a las 15:28

avatar de usuario
naveen principe p

Arreglar lo que funciona para mí es simplemente deshabilitar onReceivedSslError función definida en AuthorizationWebViewClient. En este caso handler.cancel se llamará en caso de error de SSL. Sin embargo, funciona bien con los certificados One Drive SSL. Probado en Android 2.3.7, Android 5.1.

avatar de usuario
satish baddam

De acuerdo a Alerta de seguridad de Google: implementación no segura de la interfaz X509TrustManagerGoogle Play no admitirá X509TrustManager a partir del 11 de julio de 2016:

Hola desarrollador de Google Play,

Sus aplicaciones enumeradas al final de este correo electrónico utilizan una implementación no segura de la interfaz X509TrustManager. Específicamente, la implementación ignora todos los errores de validación de certificados SSL al establecer una conexión HTTPS a un host remoto, lo que hace que su aplicación sea vulnerable a los ataques de intermediarios. Un atacante podría leer los datos transmitidos (como las credenciales de inicio de sesión) e incluso cambiar los datos transmitidos en la conexión HTTPS. Si tiene más de 20 aplicaciones afectadas en su cuenta, consulte Developer Console para obtener una lista completa.

Para manejar correctamente la validación del certificado SSL, cambie su código en el método checkServerTrusted de su interfaz personalizada X509TrustManager para generar CertificateException o IllegalArgumentException siempre que el certificado presentado por el servidor no cumpla con sus expectativas. Para preguntas técnicas, puede publicar en Stack Overflow y usar las etiquetas “android-security” y “TrustManager”.

Resuelva este problema lo antes posible e incremente el número de versión del APK actualizado. A partir del 17 de mayo de 2016, Google Play bloqueará la publicación de nuevas aplicaciones o actualizaciones que contengan la implementación no segura de la interfaz X509TrustManager.

Para confirmar que ha realizado los cambios correctos, envíe la versión actualizada de su aplicación a Developer Console y vuelva a consultar después de cinco horas. Si la aplicación no se ha actualizado correctamente, mostraremos una advertencia.

Si bien es posible que estos problemas específicos no afecten a todas las aplicaciones con la implementación de TrustManager, es mejor no ignorar los errores de validación del certificado SSL. Las aplicaciones con vulnerabilidades que exponen a los usuarios al riesgo de compromiso pueden considerarse productos peligrosos en violación de la Política de contenido y la sección 4.4 del Acuerdo de distribución para desarrolladores.

avatar de usuario
Udara Seneviratne

Tuve el mismo problema y probé todas las sugerencias mencionadas anteriormente como se muestra a continuación.

  1. Implemente onReceivedSslError() dando la oportunidad al usuario de decidir handler.proceed(); o manejador.cancel(); cuando ocurrió un error de SSL
  2. Implemente onReceivedSslError() para llamar a handler.cancel(); cada vez que se produjo un problema de SSL sin tener en cuenta la decisión del usuario.
  3. Implemente onReceivedSslError() para verificar el certificado SSL localmente además de verificar error.getPrimaryError() y proporcionar al usuario la decisión de handler.proceed(); o manejador.cancel(); solo si el certificado SSL es válido. Si no, simplemente llame a handler.cancel();
  4. Eliminando la implementación de onReceivedSslError() y simplemente dejando que ocurra el comportamiento predeterminado de Android.

Incluso después de intentar todos los intentos anteriores, Google Play seguía enviando el mismo correo de notificación mencionando el mismo error y la versión anterior de APK (aunque en todos los intentos anteriores cambiamos tanto el código de la versión como el nombre de la versión en Gradle)

Estábamos en un gran problema y nos comunicamos con el Soporte de Google por correo y le preguntamos

“Estamos cargando las versiones superiores del APK, pero el resultado de la revisión dice el mismo error que menciona la versión anterior del APK con errores. ¿Cuál es el motivo?”

Después de unos días, el soporte de Google respondió a nuestra solicitud de la siguiente manera.

Tenga en cuenta que debe reemplazar completamente la versión 12 en su pista de producción. Esto significa que tendrá que implementar por completo una versión superior para desactivar la versión 12.

El punto resaltado nunca se encontró ni se mencionó en Play Console ni en ningún foro.

De acuerdo con esa pauta en la vista clásica de Google Play, verificamos la pista de producción y había versiones con errores y la última versión con corrección de errores, pero el porcentaje de lanzamiento de la versión con corrección de errores es del 20 %. Entonces, lo convirtió en un despliegue completo y luego la versión con errores desapareció de la pista de producción. Después de más de 24 horas de tiempo de revisión, la versión ha regresado.

NOTA: Cuando tuvimos este problema, Google acababa de pasar a una nueva versión de la interfaz de usuario de su consola de juegos y se había perdido algunas vistas en la versión anterior de la interfaz de usuario o en la vista clásica. Como estábamos usando la última vista, no podíamos darnos cuenta de lo que estaba sucediendo. Simplemente, lo que sucedió fue que Google revisó la misma versión anterior con errores del APK, ya que la nueva no se implementó por completo.

avatar de usuario
Pabellón

Puede usar SslError para mostrar, alguna información sobre el error de este certificado, y puede escribir en su cuadro de diálogo la cadena del tipo de 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);    }

  • ¿No podemos simplemente llamar a handler.cancel(); dentro de onReceivedSslError? ¿Google seguirá rechazándolo?

    – DevAndroid

    14 de febrero de 2017 a las 11:34

  • @DevAndroid Google cancela esa opción porque no informa al usuario. Con este método filtrar el error muestra más información que porque la conexión ha sido rechazada

    – Pabellón

    30 de junio de 2017 a las 8:48

¿Ha sido útil esta solución?