Java EE: Seguridad en aplicaciones web (II). Evitar Inyección SQL y XSS con ESAPI

por Mar 31, 2020Desarrollo de Software0 Comentarios

Vamos a continuar con la guía de buenas prácticas de seguridad para aplicaciones web con Java EE. En esta ocasión vamos a aprender a usar la librería ESAPI de OWASP para evitar la inyección SQL y XSS (Cross Site Scripting).

Inyección SQL

Comenzaremos hablando por el primero de los riesgos de seguridad que se comentaban en la entrada anterior. Por hacer un breve recordatorio, se trata de intentar comprometer de alguna manera la base de datos de la aplicación.

Para ello, se suelen usar los formularios de las páginas web tratando de cerrar en ellos una consulta SQL e iniciando otra a continuación. Dicho esto enviar directamente los datos de los formularios hacia una consulta SQL no es una buena práctica. Veremos en este apartado una serie de buenas prácticas que deberíamos llevar a cabo para evitar la inyección SQL.

Uso de PreparedStatements

En primer lugar una muy buena práctica es cambiar el uso de los Statements por los PreparedStatements. Este tipo de Statement obliga a que la consulta tenga exactamente la estructura que se indica, y no deja que se cambie la estructura, o se cierre una consulta y se abra otra nueva. También lleva a cabo un escapado de caracteres, se podría poner de nombre «O’Donnell» y no habría problema alguno en poner la comilla simple. También se puede evitar la inyección SQL usando procedimientos almacenados.

Para hacer una consulta con un PreparedStatement lo primero que se hace es indicar la estructura de la consulta dejando los parámetros marcados con una interrogación «?». Posteriormente se irá indicando que son cada una de esas interrogaciones y su tipo de dato. Gracias a que se indica el tipo de dato, Java ya se encarga de poner las comillas que hagan falta dependiendo del tipo de dato. Veremos que hay muchos métodos set dependiendo del tipo de dato a introducir.

Veamos un ejemplo. En el ejemplo se muestra un método sencillo para hacer un insert en la base de datos . He puesto el método completo para que también se observe una buena estructura para un método de este tipo. Siguiendo esta estructura es la mejor manera de asegurarse de que todos los recursos van a ser liberados en cualquier caso, tanto si se produce un error como si la ejecución es normal. Voy a explicar al final de esta entrada en que consiste ese método cerrarConexionYStatement (), ya que considero que es un punto importante a tener en cuenta para ahorrarnos unas cuantas líneas de código.

public boolean addUser(Usuario user) {
    Connection conexion = null;
    boolean exito = false;
    PreparedStatement insert = null;
    try {
        conexion = pool.getConnection();
        //Definir la estructura de la consulta
        insert = conexion.prepareStatement("INSERT INTO " + nameBD + ".Usuarios VALUES (?,?,?,?,?)");
        //Indicamos cada uno de los parámetros
        insert.setString(1, user.getMail());
        insert.setString(2, user.getNombre());
        insert.setString(3, user.getDir());
        insert.setString(4, user.getPass());
        //Incluso si no encontramos un método set para incluir un tipo de dato se puede personalizar como en este caso
        insert.setObject(5, user.getPermisos(), java.sql.Types.CHAR, 1);
        int filasAfectadas = insert.executeUpdate();
        if (filasAfectadas == 1) {
            exito = true;
        }
    } catch (SQLException ex) {
        logger.log(Level.SEVERE, "Error insertando usuario", ex);
    } finally {
        cerrarConexionYStatement(conexion, insert);
    }
    return exito;
}

En el código de la tienda online podemos encontrar numerosos métodos de este estilo y se pueden ver ejemplos en los que se use un ResultSet. También hay algún ejemplo en el que se realizan varias transacciones dependientes entre si y se realizan rollbacks y commits manualmente.

Hay que comentar que aunque no se vea siempre se realizan commits en todos los métodos. Solo que por el hecho de ser consultas simples la conexión realiza los commit de manera automática, pero esto se puede cambiar como se puede ver en el código de la tienda online.

conexion.setAutoCommit(false);

Validar los datos de entrada con expresiones regulares

¿Esto es todo lo que podemos hacer? Pues no, podemos usar como medida de apoyo una validación de todos los datos de entrada de los usuarios en los formularios. Para realizar esta validación usaremos la librería que nos proporciona OWASP que se hace denominar ESAPI.

Ahora como es lógico veremos como usar esta librería y configurarla. En primer lugar hemos de bajar el paquete que nos proporcionan en el que se incluye en jar de ESAPI y todas las librerías que requiere ESAPI para funcionar (carpeta libs del paquete descargado). Hemos de añadir todas esas librerías a nuestro proyecto.

Una cosa importante. Entre las librerías requeridas es posible que se incluya la librería de servlets. Esa librería no la debéis de incluir en el proyecto puesto que es una versión más antigua de los Servlets y cambian algunos métodos. Nos interesa conservar la especificación de Servlets moderna que tenga nuestro servidor de aplicaciones.

La siguiente parte importante son los ficheros de configuración que necesita ESAPI para funcionar: ESAPI.properties y validation.properties. Podemos encontrar una plantilla de cada uno de los ficheros en el paquete descargado en configuration/.esapi.

La siguiente tarea es colocar los ficheros de configuración en un lugar adecuado para que se reconozcan. ESAPI buscará los ficheros en varios lugares en el orden en el que indico a continuación. Encontraremos esta información en la página de ESAPI o mirando la consola del servidor de aplicaciones.

INFO: Not found in 'org.owasp.esapi.resources' directory or file not readable: /Applications/NetBeans/glassfish-3.1/glassfish/domains/domain1/ESAPI.properties
INFO: Not found in SystemResource Directory/resourceDirectory: .esapi/ESAPI.properties
INFO: Not found in 'user.home' (/Users/Usuario) directory: /Users/Usuario/esapi/ESAPI.properties
INFO: Attempting to load ESAPI.properties via the classpath.

En mi caso me pareció lo más cómodo incluirlo en el classpath. Para ello cree en la raiz del proyecto una carpeta llamada setup, que por otra parte es estándar que los proyectos web puedan tener esta carpeta para archivos de configuraciones. Posteriormente se ha de añadir dicha carpeta al classpath del proyecto.

Evitar Inyección SQL y XSS con ESAPI

Una vez que hemos colocado los archivos en su lugar comenzaremos configurando cada uno de ellos. En estos ficheros se van a configurar las expresiones regulares que se van a usar para validar cada uno de los campos que deseemos.

El primer fichero que hemos nombrado, ESAPI.properties es un fichero que en lo que a nosotros nos concierne no hemos de tocar nada, la librería se sirve de él para hacer sus operaciones. Nuestras personalizaciones irán en el segundo fichero nombrado. Pero no está de más echar un ojo al fichero para ver las validaciones que ya incluye y se ve que incluye validación para Email y alguna otra cosa más. Mirándolo también veremos cual es la forma de las expresiones regulares que admite.

Ahora pasemos al segundo fichero en el que hemos de configurar las expresiones regulares que consideremos necesarias. Para ello hemos de poner una clave y un atributo, al estilo de un fichero de propiedades. La clave será siempre Validator.Campo, siendo Campo lo que usaremos posteriormente para referirnos a esa expresión regular.

Esto no pretende ser un tutorial sobre como crear expresiones regulares, sin embargo haré algunas anotaciones. Si alguien necesita más ayuda con el tema puede preguntar y no tendré problema alguno en resolver sus dudas.

El símbolo «^» indica el inicio de la cadena, mientras que el símbolo «$» indica el final de la cadena. Es bueno escapar caracteres como el guión y el punto puesto que el guión se usa para indicar un rango y el punto significa cualquier cosa. Otra anotación importante es que a la hora de poner caracteres regionales debéis ponerlos con su correspondiente código en UTF-16; si no se hace de esta manera después puede que no funcione correctamente. Se puede encontrar una tabla de correspondencia básica en la wikipedia. Por ejemplo, si se quiere poner una ‘a’ acentuada «á» se debe poner así \u00e1.

Si tenéis problemas con las expresiones regulares hay una web que podéis usar que puede resultar interesante. También se puede usar un software específico para expresiones regulares, nos puede ayudar a crearlas en unos sencillos pasos y a comprobar su funcionamiento. Su nombre es RegexBuddy y RegexMagic. El primero de ellos es sobretodo para aprender y verificar expresiones regulares mientras que el segundo está orientado a su creación.

Veamos ahora un pequeño ejemplo que son las que usé yo para la tienda online. Decir que no hace falta crear expresiones regulares para números porque tiene su verificación a parte que ya veremos.

Validator.Pss=^[A-Za-z0-9._$%&/()= -#@áÁéÉíÍóÓúÚüÜñÑ]+$
Validator.Name=^[A-Z][a-zA-Z -áÁéÉíÍóÓúÚüÜñÑ]+$
Validator.Adress=^[A-Z][a-zA-Z0-9\-\ \,ºáÁéÉíÍóÓúÚüÜñÑ]+\ [0-9]{5}\-[A-Z][A-Za-z\ \-áÁéÉíÍóÓúÚüÜñÑ]+$
Validator.NameDescProd=^[A-Za-z0-9.,-_ @#%&=áÁéÉíÍóÓúÚüÜñÑ¿?¡!]+$

Una vez que estos ficheros han quedado correctamente configurados pasaremos ha explicar como han de usarse dentro de nuestra aplicación.

Veamos por ejemplo un método para validar un nombre usando la expresión regular que hicimos para validar un nombre. He dejado comentarios detallados en el código. Para más información recomiendo ver la documentación disponible el javadoc de la página de los repositorios del proyecto o en el paquete descargado.

public static String validateName(String input) throws IntrusionException, ValidationException {
    Validator validador = ESAPI.validator();
    //Nombre a referirse, entrada, nombre de la expresión regular, máxima longitud, permitir nulo o no
    return validador.getValidInput("Nombre", input, "Name", 100, false);
}

El método devuelve la cadena validada, en caso de error hay dos tipos de excepciones. ValidationException es lanzada cuando simplemente no se ha pasado la validación, mientras que IntrusionException se lanza cuando se cree de manera muy clara que ha ocurrido un intento de ataque.

El primer argumento es un nombre para nuestro uso personal que posteriormente lo usará si hay un error para indicar donde ha ocurrido. Y el nombre de la expresión regular ha de ser el mismo que dimos en el fichero de propiedades, así se elige la expresión a usar para validar una entrada.

Como dije no hace falta crear expresiones regulares para los números puesto que usan validaciones a parte. Veamos un ejemplo.

public static int validateNumber(String input) throws IntrusionException, ValidationException {
    Validator validador = ESAPI.validator();
    //Nombe para referirse, entrada, mínimo, máximo, permitir nulo o no
    return validador.getValidInteger("Entero", input, 0, 999999, false);
}

Validar un número es tremendamente sencillo sin tener siquiera la necesidad de especificar una expresión regular para la validación de los mismos.

Ahora veremos un ejemplo de como usar estas validaciones dentro de nuestra aplicación.

try {
    String name = Tools.validateName(request.getParameter("name"));
} catch (IntrusionException ex) {
    request.setAttribute("resultados", "Detectada una intrusión");
    Tools.anadirMensaje(request, ex.getUserMessage());
} catch (ValidationException ex) {
    request.setAttribute("resultados", "Error en el formulario");
    Tools.anadirMensaje(request, ex.getUserMessage());
}

Escapar las entradas de usuario

ESAPI también nos permitirá escapar las entradas del usuario para introducirlas en la base de datos. La verdad es que usando PreparedStatements este paso no sería necesario. En caso de querer hacerlo habría que hacerlo después de pasar la validación por expresiones regulares, puesto que al escapar una cadena se van a incluir nuevos caracteres como la contrabarra «\». Veamos a continuación un ejemplo sencillo para escapar las entradas preparado especialmente para MySQL (ESAPI proporciona métodos de escape para MySQL y para Oracle si no recuerdo mal).

protected String scapeUserEntries (String input){
    Encoder encod = ESAPI.encoder();
    return encod.encodeForSQL(new MySQLCodec(MySQLCodec.MYSQL_MODE), input);
}

Recomiendo que partiendo de los ejemplos que he dejado aquí se eche un vistazo al javadoc de ESAPI puesto que contiene opciones bastante interesantes. Entre ellas tiene una forma de hacer validaciones seguidas, poniendo los errores en una lista para hacer todas las validaciones seguidas sin detenerse por el fallo de alguna de ellas.

Cross Site Scripting (XSS)

A modo de resumen a lo que se comentó en el capítulo anterior se podría decir, que el fin que se persigue con este tipo de ataques es introducir información en la base de datos para que posteriormente sea mostrada a otros usuarios. La información que se suele introducir serán scripts que persiguen realizar alguna actividad ilegítima.

Al igual que para evitar inyecciones SQL se validan las entradas del usuario en este caso ocurre lo mismo, también ayuda a evitar el XSS.

Aunque el problema puede venir de un caso en el que no queramos usar una expresión regular para validar. Por ejemplo, un campo de comentarios para el usuario en el que se permite cualquier caracter. En este caso lo que habrá que hacer es buscar etiquetas html sospechosas como script, pero… ¿realmente vale con buscar este tipo de etiquetas? Pues la respuesta es que no, porque si se usa otra codificación ya no van a aparecer así las etiquetas. A continuación veremos una buena forma de validar las entradas para evitar XSS con ESAPI.

El problema de las codificaciones

Lo primero veamos el problema de las codificaciones. En el siguiente ejemplo ambas líneas son equivalentes.

<script>alert("Este sitio es un peligro");</script>
%3C%2Ftitle%3E%3Cscript%3Ealert%28%22%A1Este+Sitio+es+un+peligro%21%22%29%3C%2Fscript%3E

Ahora si vemos que no se puede proteger simplemente buscando etiquetas html ¿no?. Por lo tanto lo primero que habrá que hacer es pasar la cadena a una forma canónica.

Obtener la forma canónica de una cadena consiste en convertir toda la cadena a una codificación conocida y admitida para que de esta manera se eviten problemas en la posterior validación.

Usando AntiSamy para validar entradas en HTML

ESAPI también nos proporciona soluciones para todos los problemas comentados mediante un módulo llamado AntiSamy. Se encarga de procesar código html y verificar si está permitido o por el contrario es un posible intento de intrusión. Para ello usa un fichero xml de configuración en el que se especificarán con detalle las etiquetas html permitidas y no permitidas; también será posible especificar que tipo de propiedades de CSS se pueden usar.

El archivo xml de configuración de AntiSamy se puede colocar donde nosotros queramos, aunque, obviamente no sería una buena medida colocarlo en un directorio que sea accesible publicamente. En mi caso lo he colocado en el mismo sitio que los archivos de configuración de ESAPI, en la carpeta setup añadida al classpath.

En la página del proyecto de AntiSamy es posible encontrar varios archivos de configuración de ejemplo, de más a menos restrictivos. Lo mejor es partir de alguno de estos archivos, y en caso de querer modificarlos hacerlo observando las directivas que ya contiene. Puesto que no es una tarea sencilla hacer un archivo de estos desde cero, nos proporcionan varios ejemplos. Hay un ejemplo en su página de descargas pensado para enviar datos al servidor que provienen de un TextArea con TinyMCE (Librería JQuery para dotar a un TextArea de un editor con WYSIWYG).

Ahora que hemos visto lo referente al fichero de configuración ya estamos en disposición de poner un ejemplo de un método que valida una entrada HTML. Este método valida una entrada que se le pasa como argumento y lanza una IntrusionException si la validación no ha pasado. La excepción la he lanzado yo para detectar los errores, puesto que la librería no lanza excepciones ante una validación fallida, sino que anota los errores que han ocurrido. También tiene la posibilidad de eliminar aquello que no es válido según la configuración que hayamos hecho en el fichero xml.

Es importante ver que antes de pasar la validación se obtiene la forma canónica del String usando un método que nos proporciona ESAPI. Finalmente observar como se ha indicado donde esta el fichero xml de configuración, que se ha realizado usando el classpath y obteniendo su flujo de entrada.

public static void validateHTML(String input) {
    try {
        //No se admite una entrada vacía
        if (input.isEmpty() == true) {
            throw new IntrusionException("No se admite el campo vacío", "");
        }
        //Se obtiene el archivo de configuración a través del classpath
        Policy politica = Policy.getInstance(Tools.class.getResource("/antisamy-tinymce-1.4.4.xml"));
        AntiSamy validator = new AntiSamy();
        //Antes de analizar la cadena es convertida en su forma canónica
        //ESAPI.encoder().canonicalize(input)
        CleanResults cr = validator.scan(ESAPI.encoder().canonicalize(input), politica);
        //Si ha ocurrido un error se lanza una excepción indicando el error
        if (cr.getNumberOfErrors() != 0) {
            throw new IntrusionException("Ha introducido código HTML que no está permitido",
                    cr.getErrorMessages().get(0).toString());
        }
    } catch (PolicyException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, ex.getMessage());
    } catch (ScanException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, ex.getMessage());
    }
}

Anexo – Cerrar Conexiones, Statements y ResultSets. Varargs en java

En este apartado vamos a ver una buena manera de hacer esos métodos que decíamos para cerrar las conexiones y Statments. Aquí el problema viene de hacer un método que nos sirva para todos los métodos de la clase manejadora de la base de datos. No van a tener todos los métodos un Statement o un ResultSet, pueden tener varios de cada uno.

Se puede pensar en hacer métodos que reciban listas de Statements, pero estaríamos escribiendo líneas construyendo esas listas en cada método, líneas que se supone intentamos ahorrarnos. Pues bien java tiene algo llamado varargs para facilitarnos esta tarea, aunque internamente este construyendo listas no tenemos que construirlas nosotros.

Lo único que hemos de hacer es colocar en la cabecera de los métodos de cierre unos puntos suspensivos detrás del tipo de dato. El argumento que vaya con varargs debe ser el último y debe haber solo un, eesto es debido a que sino java no tiene forma de saber los tipos de datos de los argumentos. Dentro del método podemos recorrer los elementos con un bucle for-each, como si de un lista normal se tratara, como he dicho, es lo que hace internamente.

private void cerrarConexionYStatement(Connection conexion, Statement... statements) {
    try {
        conexion.close();
    } catch (SQLException ex) {
        logger.log(Level.SEVERE, "Error al cerrar una conexión a la base de datos", ex);
    } finally {
        for (Statement statement : statements) {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException ex) {
                    logger.log(Level.SEVERE, "Error al cerrar un statement", ex);
                }
            }
        }
    }
}
private void cerrarResultSet(ResultSet... results) {
    for (ResultSet rs : results) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException ex) {
                logger.log(Level.SEVERE, "Error al cerrar un resultset", ex);
            }
        }
    }
}

Estos métodos podrían admitir toda una serie de llamadas sin depender del número de Statement o ResultSet.

cerrarConexionYStatement(conexion, select);
cerrarConexionYStatement(conexion, deleteHistorialCarros, deleteProdCarro);
cerrarResultSet(rs);
cerrarResultSet(consultaDatosCarro, rs);

Nos hemos ahorrado bastantes líneas en cada uno de los métodos. Ahora como máximo habrá dos líneas en cada método para cerrar los recursos independientemente del número de recursos que haya.


Hasta aquí ha llegado esta guía de seguridad en aplicaciones web con Java EE. Espero que haya sido de utilidad y se vean las bondades de ESAPI para evitar la inyección SQL y el XSS.

Puede venir bien la lectura de los consejos que nos da OWASP para los dos problemas que hemos tratado en esta entrada (en inglés): Inyección SQL y Cross Site Scripting (XSS).

Todo lo que hemos aprendido en estos dos capítulos se refiera a la lógica de servidor, esta lógica siempre debe existir. Pero debo comentar que si se realizan validaciones en el cliente por ejemplo usando JavaScript se puede quitar un poco de carga al servidor, ya que ante una validación fallida esa petición no llegará al servidor.

0 comentarios

Enviar un comentario

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