Aumentar el tamaño del identificativo de la cookie de sesión de Tomcat o Spring Session

Escrito por el , actualizado el .
java planeta-codigo seguridad spring
Comentarios

Tomcat
Java
Spring Boot

El protocolo HTTP es un protocolo sin estado que quiere decir que entre las peticiones no se comparte estado ni se recuerda ningún dato. Por otro lado las cookies es pequeño conjunto de datos que son almacenados en el cliente y son enviados en cada petición que se hace a un sitio web, cada sitio web mantiene su propia colección de cookies, dos sitios distintos no comparten sus cookies. Para mantener estado entre dos peticiones se hace uso de las cookies.

En Java los servidores web envían al cliente una cookie con simplemente un identificativo de la sesión, el estado se suele mantener en el servidor en memoria, en almacenamiento de disco o persistido en una base de datos como Redis. El identificativo de la sesión por defecto usando un contenedor de servlets como Tomcat tiene una longitud de 16 bytes que codificados en hexadecimal da lugar a 32 caracteres o 128 bits. Para aumentar la seguridad por si alguien intenta adivinar el identificativo de cualquier usuario que tenga sesión iniciada por fuerza bruta de casualidad es posible aumentar el número de caracteres para identificar la cookie de sesión. La clase de la API que lo permite en Tomcat es Manager.

Según Insufficient Session-ID Length un identificativo con solo 64 bits (32 de entropía) un atacante haciendo 1000 intentos por segundo y 10000 sesiones válidas tarda solo 7,15 minutos en obtener una sesión válida (32 bit = 4294967296 / 10.000 = 429496, a 1000 intentos por segundo da 429 segundos o 7,15 minutos). Con 128 bits el tiempo crece a 292 años haciendo 10000 intentos por segundo y teniendo 100000 sesiones válidas, pero podría reducirse si el número de intentos por segundo aumentase o sesiones aumentase.

Los datos se guardan en el servidor y la cookie con el identicativo de sesión no ocupa mucho aún pasando de 32 caracteres hexadecimales a una cifra mayor como 128, el número de caracteres no es significativo para el rendimiento pero se dificulta en varios órdenes de magnitud la dificultad de adivinar una cookie.

Unsando Spring Boot y Tomcat basta con usar la clase Manager para cambiar el valor por defecto de longitud de la sesión.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.github.picodotdev.springsession;

...

@SpringBootApplication
@ComponentScan("io.github.picodotdev.springsession")
public class Main {

    private static final int SESSION_ID_LENGTH = 64;

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {
        return (TomcatServletWebServerFactory factory) -> {
            factory.addContextCustomizers((Context context) -> {
                if (context.getManager() == null) {
                    context.setManager(new StandardManager());
                }
                context.getManager().getSessionIdGenerator().setSessionIdLength(SESSION_ID_LENGTH);
            });
        };
    }

    ...

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }
}

Persistiendo la sesión en Redis con Spring Sesion por defecto el identificativo de la sesión es generado a partir de un UUID, el identificativo de la sesión tiene el mismo valor por defecto de 128 bits pero para cambiar la longitud hay que proporcionar una clase que cambia el comportamiento.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.github.picodotdev.springsession;

...

@SpringBootApplication
@EnableRedisHttpSession
@ComponentScan("io.github.picodotdev.springsession")
public class Main {

    private static final int SESSION_ID_LENGTH = 64;

    @Bean
    public DefaultCookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setUseBase64Encoding(false);
        return cookieSerializer;
    }

    @Bean
    @Primary
    public RedisOperationsSessionRepository defaultSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
        return new DefaultRedisOperationSessionRespository(sessionRedisOperations);
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.springframework.session.data.redis;

...

public class DefaultRedisOperationSessionRespository extends RedisOperationsSessionRepository {

    private static final String HEX_CHARACTERS = "0123456789ABCDEF";
    private static final int MAX_INACTIVE_INTERVAL_MINUTES = 30;
    private static final int SESSION_ID_LENGTH = 127;

    private SecureRandom randomGenerator;

    public DefaultRedisOperationSessionRespository(RedisOperations<Object, Object> sessionRedisOperations) {
        super(sessionRedisOperations);
    }

    @Override
    public RedisOperationsSessionRepository.RedisSession createSession() {
        if (randomGenerator == null) {
            randomGenerator = new SecureRandom();
        }

        byte[] bytes = new byte[SESSION_ID_LENGTH];
        randomGenerator.nextBytes(bytes);
        String id = getHex(bytes);

        RedisOperationsSessionRepository.RedisSession redisSession = new RedisOperationsSessionRepository.RedisSession(new MapSession(id));
        redisSession.setMaxInactiveInterval(Duration.ofMinutes(MAX_INACTIVE_INTERVAL_MINUTES));

        return redisSession;
    }

    private String getHex(byte [] bytes) {
        if (bytes == null) {
            return null;
        }
        StringBuilder hex = new StringBuilder( 2 * bytes.length);
        for (byte b : bytes) {
            hex.append(HEX_CHARACTERS.charAt((b & 0xF0) >> 4)).append(HEX_CHARACTERS.charAt((b & 0x0F)));
        }
        return hex.toString();
    }
}
Longitud del identificativo de sesión de 64 bytes o 128 caracteres hexadecimales
1
2
D2C631033F477F9A3111F40CFDBB83DA041BC7EB4C7CD3F824349945E9CA73E660FE3E0D4DC75A685E9255F7F3C538AC1CE07ED055547CA379BA2CB7B8A52516

Un libro dedicado a la seguridad muy bueno que he leído es Iron-Clad Java Applications, tiene montón de detalles dedicados a la seguridad de las aplicaciones web sean seguras, incluido como este dedicado a la longitud de los identificativos de la sesión.

Una clave asimétrica considerada segura puedes ser de 2048 bits pero se puede generar una de hasta 8192 bits con el mismo esfuerzo lo que aumenta la seguridad de forma exponencial ante un ataque de fuerza bruta que con el aumento de la capacidad de cómputo y en el futuro puede ser viable. El tiempo de cómputo requerido por usar una clave de mayor tamaño no creo que sea significativo para la mayoría de los casos pero igualmente la seguridad aumenta.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.