Java EE: Seguridad en aplicaciones web (I)

por Mar 31, 2020Desarrollo de Software0 Comentarios

He hecho una recopilación de las partes más importantes sobre la seguridad en aplicaciones web con JavaEE para compartirlas aquí. En el proyecto de la tienda online se han puesto en práctiva varios de estos conceptos.

Al ser un tema extenso lo he distribuido entre varios artículos. En esta serie de artículos aprenderemos a usar las herramientas que nos proporcionan las librerías de la empresa OWASP. También aprovecharemos para fijarnos en sus consejos, empresa dedicada a estudiar los diferentes problemas de seguridad.

Principios de seguridad en aplicaciones web

Antes de nada vamos a dar una guía de buenas prácticas que es interesante conocer antes de empezar a construir aplicaciones para conseguir un trabajo lo más bueno posible.

  • No dar nunca nada por hecho ni en cuestiones de seguridad ni en cuestiones del flujo normal de la aplicación. Todo el riesgo que se corra debe ser por parte del usuario (no hay nada que hacer contra eso). Me explico, no supongamos que si le pido el nombre al usuario no me va a poner un número de teléfono. No se si sería una forma acertada de decirlo pero hay que pensar con pesimismo, en los peores casos, y por remotos que sean pueden ocurrir.
  • Siempre que usemos servicios externos estamos asumiendo riesgos añadidos. Podemos haber hecho una página web muy segura y muy bien construida, pero si incrustamos contenido externo nadie nos asegura que el contenido externo sea vulnerable a algún tipo de ataque.
  • La oscuridad no es seguridad. No poner un botón acceso a la administración no impide que se pueda acceder a ella. Ocultar nuestro código no debe ser parte de nuestra seguridad.
  • Principio del mínimo privilegio: El usuario del sistema debe tener únicamente los privilegios que necesita para llevar a cabo su actividad.
  • Fallar de manera segura: Hay que tratar de manera muy cuidadosa los fallos en la aplicación. Por poner un ejemplo, si se produce un fallo en la aplicación mientras se realizan tareas administrativas no debe seguir iniciada la sesión como administrador. Otro ejemplo, no debe mostrar en un fallo información técnica sobre el mismo al usuario del sistema. Si el usuario sabe datos acerca de nuestro sistema podría tener más fácil la búsqueda de vulnerabilidades.

Los riesgos

Una vez vistas las buenas prácticas en el desarrollo de aplicaciones pasamos a explicar los riesgos de seguridad en aplicaciones web. No vamos a ponernos a programar nada sin saber los riesgos existentes y el porque de las medidas para combartirlos.

  • Inyección SQL: Consiste en intentar «engañar» al sistema para que realice peticiones contra la base de datos que no son las que han sido programadas y que podrían comprometer gravemente la base de datos o incluso mostrar al atacante toda la información de la misma.
  • Cross Site Scripting: El atacante intentará enviar información a nuestro servidor por medio de nuestros formularios u otros medios con la intención de que dicha información sea almacenada en nuestra base de datos y posteriormente sea mostrada a los demás usuarios del sistema. Un ejemplo sencillo, un código JavaScript que borre el contenido de la página, si eso es mostrado a los demás usuarios de la aplicación verán siempre una página en blanco. Esto es un ejemplo sencillo, pero imaginarios que lo que se consigue introducir es un código que tome el control de los navegadores de los usuarios de la aplicación web.
  • Robo de sesión: Como sabemos HTTP es un protocolo sin estados, lo que significa que las credenciales o información de sesión deberá ir en cada petición, debido a esto dichos datos resultan muy expuestos. Un robo de estos datos podría tener como resultado que alguien se estuviera haciendo pasar por nosotros y realizando acciones con unos privilegios que nos pertenecen. Tampoco hemos de olvidar que se puede robar la sesión intentando obtener nuestros credenciales de alguna manera (averiguar nuestra contraseña).
  • Acceso a URLs restringidas: Consiste en la observación de una URL e intentar cambiarla para intentar acceder a otras zonas. Estas es una de las razones por las que la seguridad a través de la ocultación no es efectiva.

Solucionando los problemas de seguridad

Una vez identificados los principales problemas de seguridad en aplicaciones web iremos viendo uno por uno junto con las soluciones para aplicaciones con JavaEE.

Robo de sesión

Ya hemos visto el riesgo que tiene el robo de una sesión. Podríamos decir de manera resumida que el peligro está en la exposición de los datos de sesión.

Robo de Sesión. Seguridad en aplicaciones web JavaEE
Ejemplo ilustrativo de posible robo de sesión

Para solucionar este problema de seguridad hay que atenerse a varios aspectos de la seguridad: la autentificación y la sesión. Para cada uno de ellos veremos varios aspectos importantes a cubrir para solucionar problemas con el robo de sesión.

La Autentificación

  • El más importante de todos. Usar SSL sobre HTTP (HTTPS) para transferir los datos y asegurarse de que el cifrado cubre los credenciales y el ID de sesión en todas las comunicaciones. De esta manera, los datos de sesión siguen estando expuestos pero esta vez se encuentran cifrados, por lo que no se pueden usar. Alguien podría pensar: «¿Y si se obtiene la sesión y se rompe la encriptación?» La respuesta es sencilla, y es que con los medios actuales para cuando hayas conseguido romper la encriptación esa sesión habrá dejado de existir.
  • Usar un sistema de autentificación simple, centralizado y estandarizado. Es mejor que usemos métodos de autentificación que nos proporcione el propio servidor de aplicaciones en vez de soluciones implementadas por nosotros. Lo que implementa el servidor de aplicaciones es usado en muchos lugares y ha sido suficientemente probado. Por ejemplo los filtros de Java EE (que veremos posteriormente) o los métodos de autentificación que proporciona Java EE (no los he usado).
  • Posibilitar el bloqueo de autentificación después de un número determinado de intentos fallidos. Esto podría evitar ataques de fuerza bruta intentando averiguar la contraseña del usuario.
  • Implementar métodos seguros de recuperación de contraseñas: Es común que se intenten usar estos métodos para intentar ganar acceso a una cuenta del usuario. Estos son varios consejos para implementar estos métodos de manera segura.
    • Pedir al usuario al menos tres datos o más, obligar a que responda preguntas de seguridad.
    • La contraseña que se recupera deberá generarse de manera aleatoria (con una longitud de al menos 8 caracteres) y enviada al usuario por un canal diferente (E-mail). De esta forma, si el atacante consiguió sortear los primeros pasos es difícil que logre sortear el canal usado para transmitir.

La Sesión

  • Usar los métodos de sesión que nos proporciona el servidor de aplicaciones que estemos usando. Digo esto por las mismas razones que aconsejé usar los métodos de autentificación que proporciona el servidor de aplicaciones. En este caso nos estamos refiriendo a la sesión y a las cookies.
  • Asegurar que la operación de cierre de sesión realmente destruye dicha sesión. También fijar el periodo de expiración de la sesión (periodo de tiempo en el que no se realice ninguna acción bajo dicha sesión). Por ejemplo para aplicaciones críticas de 2 a 5 minutos, mientras que para otras aplicaciones más comunes se podría usar de 15 a 30 minutos.

En el descriptor de despliegue de JavaEE podemos fijar la caducidad de la sesión en minutos.

<session-config>
    <session-timeout>15</session-timeout>
</session-config>

Poniendo en práctica los mecanismos de seguridad con Java EE

Hemos visto las formas de evitar el robo de sesión, o al menos de manera teórica. Ahora vamos a pasar a poner las cosas en práctica

Hemos hablado de usar métodos proporcionados por el servidor de aplicaciones para realizar la autenticación usando filtros y la sesión. Vamos a ver en concreto como podría hacerse. En primer lugar habrá que comprobar si los datos de usuario son correctos, y posteriormente se podría hacer algo como añadir algún parámetro a la sesión indicando que esta autenticada. Voy a simplificar mucho un código para que nos centremos en lo fundamental.

if (password.equals(user.getPass()) == true) {
    //Se añade a la sesión un boolean indicando que está autentificado
    request.getSession().setAttribute("auth", true);
    //Se añade a la sesión el identificador del usuario
    request.getSession().setAttribute("usuario", user.getMail());
}

Respecto a bloquear el inicio de sesión después de un número determinado de intentos fallidos podría ser tan fácil como añadir a la sesión el número de intentos fallidos. En el caso de que superen un determinado número no ejecutar ningún mecanismo de autenticación ni mostrar el formulario de login. Tan sencillo como lanzar un timer para desbloquear el inicio de sesión pasado un tiempo.

protected void starTimer(final HttpSession sesion) {
    //Tarea que se lanzará cuando el Timer la ejecute
    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            sesion.invalidate();
        }
    };
    Timer timer = new Timer ();
    //El tiempo esta en milisegundos y se lanza la tarea que definimos anteriormente
    timer.schedule(timerTask, 600000);
}

Para comprobar este parámetro que hemos añadido a la sesión para indicar que está autenticada lo mejor usar filtros para todas las URLs para las que se necesite permiso, y en ellos comprobar si existe el parámetro añadido en la sesión o no.

Lo primero de todo en el descriptor de despliegue (web.xml) hemos de configurar el filtro. Definiremos el filtro para un patrón de URL, en este caso todas las estén dentro del directorio /admin. Como sabemos los filtros actúan ante las peticiones de cliente para los patrones de URL para los que estén definidos. ¿Es esto del todo cierto? Pues no, en Java EE 6 esto ha cambiado un poco y podemos especificar que un filtro se ejecute sin necesidad de que el cliente haga una petición. Simplemente con que el servidor haga una redirección porque así lo especifique el código (repito, sin que el cliente tenga nada que ver y sin que sepa nada de esa redirección). Podemos hacer esto mediante las sentencias que he dejado resaltadas.

<!-- Filtro de zona privada-->
<filter>
    <filter-name>AdminFilter</filter-name>
    <filter-class>control.admin.AdminFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>AdminFilter</filter-name>
    <url-pattern>/admin/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

Ahora veremos la clase que implementa el filtro y veremos que es a la misma clase indicada en el descriptor de despliegue. En ella simplemente se comprueba la existencia de los parámetros que se añadieron a la sesión en el proceso de autenticación. Los nombres de las variables son claros por lo que no he hecho comentarios, me parece que se ve claro el objetivo del código

Unicamente comentar que un Servlet no es exclusivo de una aplicación web. Un ServletHttp es una clase de Servlet especial por así decirlo, de ahí que con el ServletRequest que nos da el filtro no podamos obtener la sesión y por eso necesitamos la primera línea del método doFilter en la que se hace un casting.

package control.admin;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
 * @author Juan Díez-Yanguas Barber
 */
public class AdminFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //Obtenemos un HttpServletRequest con un casting
        HttpServletRequest requestMod = ((HttpServletRequest) request);
        if (isPermited(requestMod) == false){
            requestMod.getSession().setAttribute("requestedPage", requestMod.getRequestURL().toString());
            RequestDispatcher noPermited = request.getRequestDispatcher("/WEB-INF/paginasError/restricted.jsp");
            noPermited.forward(request, response);
        }else{
            //Continua la secuencia de ejecución normal
            chain.doFilter(request, response);
        }
    }
    private boolean isPermited(HttpServletRequest request) {
        if (request.getSession().getAttribute("auth") == false || request.getSession().getAttribute("usuario") == null) {
            return false;
        } else{
            return true;
        }
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void destroy() {
    }
}

Ahora veremos como cerrar la sesión de la manera más correcta. Lo más correcto sería destruir la sesión, de esta manera nuestra sesión dejará de existir a todos los efectos. Para ello usaremos la siguiente sentencia, cuya función es destruir la sesión. La podríamos usar por ejemplo en un Servlet encargado del cierre de sesión.

request.getSession().invalidate();

Respecto al tema de las contraseñas voy a dedicar al final de esta entrada un apartado donde explicaré como trabajar correctamente con ellas.

Acceso a URLs restringidas

Como ya hemos visto en el apartado teórico sobre este problema la seguridad a través de la ocultación no sirve de nada. Si por ejemplo para la parte pública se usara un patrón de URL /public el atacante podría pensar que la parte privada pudiera ser /admin o cosas parecidas. Con no dar a conocer el detalle de ese directorio no es suficiente, hay que protegerlo.

Como a estas alturas supondremos no hay que hacer nada especial si se ha realizado lo anterior, es decir, si hemos realizado una autenticación y un control de sesión correctos. Con el uso de los filtros podríamos solucionar este problema perfectamente.

Se que es un problema que a la vista parece bastante evidente y con una solución sencilla. Pero mientras siga apareciendo como uno de los problemas más graves de seguridad será porque no está tan bien solucionado aunque sea evidente el problema y la solución.

Trabajando correctamente con contraseñas

Las contraseñas son un dato delicado, la palabra secreta que usa el usuario para acceder a su cuenta en el sitio web, algo suyo y personal. Por este motivo únicamente el usuario debería conocer su contraseña. Ahora bien, si solo conoce él la contraseña… ¿Cómo la comprobamos cuando inicie sesión? Lo haremos usando una huella digital MD5 o SHA1 u otros mecanismos de huella digital más modernos y seguros.

En definitiva, estos dos son los algoritmos más usados para la generación de huellas digitales. Siempre generan una cadena de la misma longitud independientemente del tamaño del mensaje. Con la particularidad de que no se puede volver al mensaje original a partir del mensaje encriptado, pero un mismo mensaje siempre genera el mismo código.

//Método para generar la huella MD5
public static String generateMD5Signature(String input) {
    try {
        //Cambiando MD5 por SHA-1 podríamos obtener la huella usando este otro algoritmo
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] huella = md.digest(input.getBytes());
        return transformaAHexadecimal(huella);
    } catch (NoSuchAlgorithmException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE,
                "No se ha encontrado el algoritmo MD5", ex);
        return "-1";
    }
}
//Método para transformar el array de bytes en una cadena hexadecimal
private static String transformaAHexadecimal(byte buf[]) {
    StringBuilder strbuf = new StringBuilder(buf.length * 2);
    for (int i = 0; i < buf.length; i++) {
        if (((int) buf[i] & 0xff) < 0x10) {
            strbuf.append("0");
        }
        strbuf.append(Long.toString((int) buf[i] & 0xff, 16));
    }
    return strbuf.toString();
}

A sabiendas de lo anterior podríamos darnos cuenta de que si en vez de guardar la contraseña de los usuarios guardamos la huella digital de la misma podremos verificar su identidad cuando él inicie sesión. Así tendremos su contraseña guardada ni modo alguno de obtenerla. Lo que habrá que hacer es obtener la huella de su contraseña cuando el usuario la introduzca y la compararemos con la huella que nosotros teníamos guardada.

Así estaremos protegiendo al usuario; si nuestra base de datos se viese comprometida y llegamos a tener guardadas las contraseñas todos nuestros usuarios estarían expuestos. Mientras que de esta manera que he explicado esto no ocurriría.

¿Es este método infalible? Pues no, existen tablas de huellas de palabras clave que se suelen usar. Ya se sabe que no solemos ser bastante ocurrentes poniendo las palabras clave (solemos usar 1234, password, etc…). De manera que si disponen de uno de estos diccionarios podrían obtener las contraseñas a partir de las huellas de nuestra base de datos comprometida (si bien no obtendrán todas, alguna seguro que sí).

Pues bien, aún con estos problemas nos quedan armas a usar. Si en vez de guardar las huellas de las contraseñas, guardamos las huellas de las contraseñas sumado con algo más. Si obtienen el mensaje que formó esa huella con una tabla de esas obtendrían la contraseña más eso que añadimos nosotros, luego no obtienen la contraseña. Esto podría admitir muchas variaciones, como poner caracteres por enmedio de la contraseña, al final, al principio, lo que se añada a la contraseña podría ser tan complejo como desearamos. Eso sí, no hay que olvidar que al comprobar la contraseña hay que hacer el proceso inverso.

Veamos un ejemplo sencillo, mostraremos como se introduce la contraseña cuando el usuario se registra y como se comprueba cuando inicia sesión. En este caso vamos a añadir al final de la contraseña el nombre de usuario del sistema (podría haber problemas si se cambiara el nombre de usuario en la máquina, pero no me voy a centrar en eso ahora, esto pretende ser solo un ejemplo ilustrativo).

String pass = request.getParameter("pass");
pass = Tools.generateMD5Signature(pass + System.getProperty("user.name"));

Veamos ahora el proceso de comprobación (user será el usuario que tenemos guardado ya en la base de datos y del cual tenemos su huella de la contraseña más el añadido).

String pass = request.getParameter("pass");
if (Tools.generateMD5Signature(password + System.getProperty("user.name")).equals(user.getPass()) == true) {
    //Contraseña correcta
} else {
    //Contraseña incorrecta
}

Ahora la cuestión viene cuando hacemos un método para recuperar la contraseña, no la podemos recuperar de forma alguna, no la hemos guardado. Por tanto, lo que habrá que hacer es generar una nueva y asignarla al usuario y mandarle la nueva contraseña por correo.

//Como argumento la longitud de la contraseña
String newPass = RandomStringUtils.randomAlphanumeric(19);
String newPassHash = Tools.generateMD5Signature(newPass + System.getProperty("user.name"));
//Asignar newPassHash al usuario

Hasta aquí llegamos con este artículo, como ya he comentado, trataré los otros problemas de seguridad en artículos posteriores. Espero que os haya sido de utilidad y os ayude a hacer aplicaciones web más seguras. También podeis encontrar más información en la web de OWASP sobre la autentificación y sobre más cosas que hemos estado tratando.

Digo lo mismo de siempre, estoy a vuestra disposición para dudas, comentarios o lo que sea; por supuesto también podeis aportar alguna otra cosa que consideréis interesante.

0 comentarios

Enviar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *