Prevención de inyecciones SQL en Java con JPA e Hibernate

publicado original en inglés en DZone 09.2022

Cuando echamos un vistazo al top 10 de vulnerabilidades de OWASP [1], las Inyecciones SQL siguen en una posición popular. En este breve artículo, discutimos varias opciones sobre cómo las Inyecciones SQL pueden ser evitadas.

Cuando las aplicaciones tienen que lidiar con bases de datos existentes siempre preocupaciones de alta seguridad, si un invasor tiene la posibilidad de secuestrar la capa de base de datos de su aplicación, puede elegir entre varias opciones. Robar los datos de los usuarios almacenados para inundarlos de spam no es el peor escenario que podría darse. Aún más problemático sería que se abusara de la información de pago almacenada. Otra posibilidad de un ciberataque SQL Injection es conseguir acceso ilegal a contenidos y/o servicios de pago restringidos. Como podemos ver, hay muchas razones por las que preocuparse por la seguridad de las Aplicaciones (Web).

Para encontrar medidas preventivas eficaces contra las inyecciones SQL, primero debemos comprender cómo funciona un ataque de inyección SQL y a qué puntos debemos prestar atención. En resumen: cada interacción de usuario que procesa la entrada sin filtrar en una consulta SQL es un posible objetivo para un ataque. La entrada de datos puede ser manipulada de manera que la consulta SQL enviada contenga una lógica diferente a la original. El listado 1 le dará una buena idea de lo que podría ser posible.

SELECT Username, Password, Role FROM User
   WHERE Username = 'John Doe' AND Password = 'S3cr3t';
SELECT Username, Password, Role FROM Users
   WHERE Username = 'John Doe'; --' AND Password='S3cr3t';
SELECT Username, Password, Role FROM User
   WHERE Username = 'John Doe' AND Password = 'S3cr3t';
SELECT Username, Password, Role FROM Users
   WHERE Username = 'John Doe'; --' AND Password='S3cr3t';
SQL
SQL

Listing 1: Simple SQL Injection

La primera sentencia del Listado 1 muestra la consulta original. Si no se filtra la entrada para las variables Username y Password, tenemos una falta de seguridad. La segunda consulta inyecta para la variable Username un String con el nombre de usuario John Doe y se extiende con los caracteres ‘; -. Esta sentencia se salta la rama AND y da, en este caso, acceso al login. La secuencia ‘; cierra la sentencia WHERE y con – todos los caracteres siguientes quedan sin comentar. Teóricamente, es posible ejecutar entre ambas secuencias de caracteres cualquier código SQL válido.

Por supuesto, mi plan no es difundir las ideas de que los comandos SQL podrían suscitar las peores consecuencias para la víctima. Con este simple ejemplo, asumo que el mensaje es claro. Necesitamos proteger cada variable de entrada UI en nuestra aplicación contra la manipulación del usuario. Incluso si no se utilizan directamente para consultas a la base de datos. Para detectar esas variables, siempre es una buena idea validar todos los formularios de entrada existentes. Pero las aplicaciones modernas suelen tener más que unos pocos formularios de entrada. Por esta razón, también menciono mantener un ojo en sus puntos finales REST. A menudo sus parámetros también están conectados con consultas SQL.

Por esta razón, la validación de entradas, en general, debería formar parte del concepto de seguridad. Las anotaciones de la especificación Bean Validation [2] son, para este propósito, muy potentes. Por ejemplo, @NotNull, como Anotación para el campo de datos en el objeto de dominio, asegura que el objeto sólo es capaz de persistir si la variable no está vacía. Para utilizar las Anotaciones de Validación de Bean en tu proyecto Java, sólo necesitas incluir una pequeña librería.

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>${version}</version>
</dependency>
XML

Listing 2: Maven Dependency para Bean Validation

Quizá sea necesario validar estructuras de datos más complejas. Con las expresiones regulares, tiene otra poderosa herramienta en sus manos. Pero tenga cuidado. No es tan fácil escribir una RegEx que funcione correctamente. Veamos un pequeño ejemplo.

public static final String RGB_COLOR = "#[0-9a-fA-F]{3,3}([0-9a-fA-F]{3,3})?";

public boolean validate(String content, String regEx) {
    boolean test;
    if (content.matches(regEx)) {
        test = true;
    } else {
        test = false;
    }
    return test;
}

validate('#000', RGB_COLOR);
Java

Listing 3: Validación mediante expresiones regulares en Java

El RegEx para detectar el esquema de color RGB correcto es bastante simple. Las entradas válidas son #ffF o #000000. El rango para los caracteres es 0-9, y las letras A a F. Insensible a mayúsculas y minúsculas. Cuando desarrollas tu propio RegEx, siempre necesitas comprobar muy bien los límites existentes. Un buen ejemplo es también el formato de 24 horas. Errores típicos son entradas inválidas como 23:60 o 24:00. El método validate compara la cadena de entrada con el RegEx. Si el patrón coincide con la entrada, el método devolverá true. Si quieres obtener más ideas sobre validadores en Java, también puedes consultar mi repositorio de GitHub [3].

En resumen, nuestra primera idea para asegurar la entrada del usuario contra el abuso es filtrar todas las secuencias de caracteres problemáticas, como – y así sucesivamente. Bueno, esta intención de crear una lista de bloqueo no es tan mala. Pero sigue teniendo algunas limitaciones. Al principio, la complejidad de la aplicación aumentó porque bloquear caracteres individuales como -; y ‘ podría causar a veces efectos secundarios no deseados. Además, una limitación de caracteres por defecto en toda la aplicación puede causar problemas. Imagina que hay un área de texto para un sistema de Blog o algo similar.

Esto significa que necesitamos otro concepto potente para filtrar la entrada de forma que nuestra consulta SQL no pueda manipularla. Para alcanzar este objetivo, el estándar SQL tiene una gran solución que podemos utilizar. Los Parámetros SQL son variables dentro de una consulta SQL que serán interpretadas como contenido y no como una sentencia. Esto permite que los textos grandes bloqueen algunos caracteres peligrosos. Echemos un vistazo a cómo funcionará esto en una base de datos PostgreSQL [4].

DECLARE user String;
SELECT * FROM login WHERE name = user;
SQL

Listing 4: Definición de parámetros en PostgreSQL

En el caso de que esté utilizando el mapeador OR Hibernate, existe una forma más elegante con la Java Persistence API (JPA).

String myUserInput;

@PersistenceContext
public EntityManager mainEntityManagerFactory;

CriteriaBuilder builder =
    mainEntityManagerFactory.getCriteriaBuilder();

CriteriaQuery<DomainObject> query =
    builder.createQuery(DomainObject.class);

// create Criteria
Root<ConfigurationDO> root =
    query.from(DomainObject.class);

//Criteria SQL Parameters
ParameterExpression<String> paramKey =
    builder.parameter(String.class);

query.where(builder.equal(root.get("name"), paramKey);

// wire queries together with parameters
TypedQuery<ConfigurationDO> result =
    mainEntityManagerFactory.createQuery(query);

result.setParameter(paramKey, myUserInput);
DomainObject entry = result.getSingleResult();
Java

Listing 5: Uso de parámetros SQL de Hibernate JPA

El listado 5 se muestra como un ejemplo completo de Hibernate utilizando JPA con la API de criterios. La variable para la entrada del usuario se declara en la primera línea. Los comentarios en el listado explican cómo funciona. Como puedes ver, no es ninguna ciencia espacial. La solución tiene otras ventajas además de mejorar la seguridad de la aplicación web. Al principio, no se utiliza SQL plano. Esto asegura que cada sistema de gestión de bases de datos soportado por Hibernate puede ser asegurado por este código.

Puede que el uso parezca un poco más complejo que una simple consulta, pero el beneficio para tu aplicación es enorme. Por otro lado, por supuesto, hay algunas líneas extra de código. Pero no son tan difíciles de entender.

Recursos

Links are only visible for logged in users.

Date vs. Boolean

El modelado de tablas de bases de datos puede dar lugar rápidamente a redundancias que...

Date vs. Boolean

Cuando diseñamos modelos de datos y sus correspondientes tablas a veces aparece Boolean como tipo de dato. En general estos indicadores no son realmente problemáticos. Pero tal vez podría haber una mejor solución para el diseño de datos. Permítanme darles un breve ejemplo de mi intención.

Supongamos que tenemos que diseñar un dominio simple para almacenar artículos. Como un Sistema de Blog o cualquier otro Gestor de Contenidos. Además del contenido del artículo y el nombre del autor, podríamos necesitar una bandera que indique al sistema si el artículo es visible para el público. Algo así como publicado como un booleano. Pero también hay un requisito de cuando el artículo está programado una fecha para su publicación. En la mayoría de los diseños de bases de datos observé para esas circunstancias un Booleano: published y una Fecha: publishingDate. En mi opinión este diseño es un poco redundante y también propenso a errores. Como conclusión rápida me gustaría aconsejarte que utilices desde el principio sólo Date en lugar de Boolean. El escenario que he descrito anteriormente también puede transformarse en muchas otras soluciones de dominio.

Por ahora, después de tener una idea de por qué debemos sustituir Boolean por Date datatype nos centraremos en los detalles de cómo podemos alcanzar este objetivo.

Tratar con SQL estándar sugiere que reemplazar un Sistema de Gestión de Bases de Datos (SGBD) por otro no debería ser un gran problema. Desgraciadamente, la realidad es un poco diferente. No es recomendable utilizar todos los tipos de datos disponibles para fechas como Timestamp. Por experiencia prefiero usar el simple java.util.Date para evitar futuros problemas y otras sorpresas. El formato almacenado en la tabla de la base de datos se parece a: ‘YYYY-MM-dd HH:mm:ss.0’. Entre la Fecha y la Hora hay un espacio y .0 indica un offset. Este desfase describe la zona horaria. La zona horaria estándar de Europa Central CET tiene un desfase de una hora. Eso significa UTC+01:00 en formato internacional. Para definir el offset por separado obtuve buenos resultados utilizando java.util.TimeZone, que funciona perfectamente junto con Date.

Antes de continuar os voy a mostrar un pequeño fragmento de código en Java para el gestor OR de Hibernate y cómo podríais crear esas columnas de la tabla.

@Table(name = "ARTICLE")
public class ArticleDO {

    @CreationTimestamp
    @Column(name = "CREATED")
    @Temporal(TemporalType.DATE)
    private Date created;

    @Column(name = "PUBLISHED")
    @Temporal(TemporalType.DATE)
    private Date published;

    @Column(name = "DEFAULT_TIMEZONE")
    private String defaultTimezone;

    //Constructor
    public ArticleDO() {
        TimeZone.setDefault(Constraints.SYSTEM_DEFAULT_TIMEZONE);
        this.defaultTimezone = "UTC+00:00";
        this.published = new Date('0000-00-00 00:00:00.0');
    }

    public Date isPublished() {
        return published;
    }

    public void setPublished(Date publicationDate) {
    	if(publicationDate != null) {
        	this.published = publicationDate;
    	} else {
    		this.published = new Date(System.currentTimeMillis());
    	}
    }
}  
Java
INSERT INTO ARTICLE (CREATED, PUBLISHED, DEFAULT_TIMEZONE)
    VALUES ('1984-04-01 12:00:01.0', '0000-00-00 00:00:00,0', 'UTC+00:00);
SQL

Veamos un poco más de cerca el listado anterior. Primero vemos la anotación @CreationTimestamp. Esto significa que cuando el objeto ArticleDO se crea, la variable creada se inicializa con la hora actual. Este valor nunca debe cambiar, porque un artículo puede ser creado una sola vez pero cambiado varias veces. La Zona Horaria se almacena en un String. En el Constructor puedes ver como la Zona Horaria del sistema puede ser tomada – pero ten cuidado este valor no debe confiarse mucho. Si tienes un usuario como yo que viaja mucho, verás que en todos los lugares en los que estoy tengo la misma hora del sistema, porque normalmente nunca la cambio. Como zona horaria por defecto defino la cadena correcta para UTC-0. Lo mismo hago para la variable publicada. Date también puede ser creada por un String lo que usamos para establecer nuestro valor cero por defecto. El Setter para published tiene la opción de definir una fecha futura o usar la hora actual en el caso de que el artículo se publique inmediatamente. Al final del listado demuestro una simple importación SQL para un solo registro.

Pero no hay que precipitarse. También tenemos que prestar un poco de atención a cómo tratar con el desplazamiento UTC. Debido a que he observado en los sistemas enormes varias veces los problemas que se produjeron porque el desarrollador se utilizó sólo los valores predeterminados.

La zona horaria en general forma parte del concepto de internacionalización. Para gestionar correctamente los ajustes de desfase podemos decidir entre diferentes estrategias. Como en tantos otros casos, no hay un claro correcto o incorrecto. Todo depende de las circunstancias y necesidades de su aplicación. Si se trata de un sitio web de ámbito nacional, como el de una pequeña empresa, y no hay eventos críticos en el tiempo, todo resulta muy sencillo. En este caso no será problemático gestionar la configuración de la zona horaria automáticamente por el DBMS. Pero hay que tener en cuenta que en el mundo existen países como México con más de una zona horaria. Un sistema internacional donde los clientes se extienden por todo el mundo podría ser útil para configurar cada DBMS en el clúster a UTC-0 y gestionar el desplazamiento por la aplicación y los clientes conectados.

Otra cuestión que debemos resolver es cómo inicializar el valor de la fecha de un registro por defecto. Porque los valores nulos deben evitarse. Una explicación completa de por qué devolver null no es un buen estilo de programación se encuentra en libros como ‘Effective Java’ y ‘Clean Code’. Tratar con Excepciones de Puntero Nulo es algo que realmente no necesito. Una buena práctica que funciona bien para mí es una fecha por defecto – valor de tiempo por ‘0000-00-00 00:00:00.0’. Así evito publicaciones no deseadas y el significado es muy claro para todos.

As you can see there are good reasons why Boolean data types should replaced by Date. In this little article I demonstrated how easy you can deal with Date and timezone in Java and Hibernate. It should also not be a big thing to convert this example to other programming languages and Frameworks. If you have an own solution feel free to leave a comment and share this article with your colleagues and friends.