- Introducción: La Complejidad de los Filtros de Sesión
Durante una auditoría de seguridad en un sistema basado en Java, se identificó un componente crítico: un filtro de sesión (similar a SessionFilter). Este tipo de filtro actúa como la primera línea de defensa en el control de acceso, siendo responsable de validar las solicitudes entrantes y asegurar que solo los usuarios autenticados accedan a recursos protegidos. Sin embargo, su implementación reveló varias deficiencias de seguridad que podrían ser explotadas para eludir los controles de autenticación.
- Fallas en la Lista Blanca de Rutas
2.1. Lógica de Validación Permisiva
El sistema empleaba una lista blanca de rutas que supuestamente permitía el acceso sin autenticación a ciertas URLs. La lógica para esta validación presentaba una vulnerabilidad:
public static boolean esRutaPublica(String rutaSolicitada, HttpServletRequest req) {
String[] rutasExcluidas = new String[]{"/inicio_sesion.jsp", "/api/usuarios/acceso.do", "/servicio/", /* ... */};
String contextoApp = req.getContextPath(); // e.g., /miApp
for (String rutaExcluida : rutasExcluidas) {
// La comparación se realiza uniendo el contexto con la ruta excluida
if (rutaSolicitada.startsWith(contextoApp + rutaExcluida)) {
return true; // Acceso permitido sin validación de sesión
}
}
return false;
}
La debilidad radica en que cualquier URL que comience con una de las entradas de la lista blanca, incluso después de la concatenación con contextPath, se considera válida. Esto abre la puerta a técnicas de derivación de rutas.
2.2. Elusión por Normalización de Rutas
Un atacante podría explotar la forma en que los servidores web o los marcos de trabajo normalizan las rutas URL. Por ejemplo, al inyectar secuencias como ../, la ruta solicitada puede transformarse de una manera que coincida con una entrada de la lista blanca.
Ejemplo de Ataque:
- Una URL maliciosa como
/aplicacion/recursos/../inicio_sesion.jsp - Podría ser normalizada internamente a
/aplicacion/inicio_sesion.jsp
Si /inicio_sesion.jsp está en la lista blanca y la comparación es por prefijo, el atacante lograría eludir la autenticación y acceder a recursos restringidos.
- Deficiencias en los Mecanismos de Control Adicionales
3.1. Validación de Referer Falsificable
El filtro implementaba una comprobación del encabezado Referer para la mayoría de las solicitudes, excepto para un subconjunto específico de rutas. No obstante, el encabezado Referer es fácilmente manipulable por un atacante.
public boolean esRefererValido(String pathActual, HttpServletRequest req) {
String[] rutasExentas = new String[]{"/api/sso/login"};
String contextoApp = req.getContextPath();
// Solo se exime la ruta especificada
for (String exenta : rutasExentas) {
if (pathActual.startsWith(contextoApp + exenta)) {
return true; // No se valida Referer para esta ruta
}
}
String referer = req.getHeader("Referer");
// Lógica de validación débil o inexistente para el resto...
// Si Referer es nulo o no se valida contra un dominio esperado, podría ser omitido.
return (referer != null && referer.contains("dominioSeguro.com")); // Ejemplo simplificado
}
Un atacante puede simplemente configurar el encabezado Referer: http://dominioSeguro.com/ para pasar esta verificación, incluso si la solicitud proviene de una fuente no autorizada.
3.2. Dependencia de Configuraciones Externas
Ciertas reglas de seguridad dependían de banderas de configuración globales, como interruptores para sistemas de terceros. Esto introduce un riesgo significativo:
if (ConfiguracionGlobal.SERVICIO_EXTERNO_ACTIVO && "PROD".equals(ConfiguracionGlobal.ENTORNO_TIPO)
&& this.esAccesoRestringido(rutaActual, req)) {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
return;
}
La seguridad del sistema no debería depender de configuraciones que puedan ser inconsistentes en entornos de alta concurrencia o que puedan modificarse dinámicamente sin un control de seguridad estricto.
3.3. Lógica Defectuosa en esAccesoRestringido
La función encargada de determinar si el acceso debe ser restringido (esAccesoRestringido) tenía condiciones muy específicas, creando grandes brechas para la mayoría de los puntos finales de la API.
private boolean esAccesoRestringido(String pathRecurso, HttpServletRequest req) {
if (pathRecurso.contains("extension/flujo")) {
// Solo restringe métodos que no sean GET
String metodoHttp = req.getMethod();
return !"GET".equalsIgnoreCase(metodoHttp);
}
// Restricciones para otras rutas específicas
String[] rutasCriticas = new String[]{"extension/confianza"};
for (String critica : rutasCriticas) {
if (pathRecurso.contains(critica)) {
// Aplica alguna lógica de restricción aquí, si la hay
return true;
}
}
return false; // Por defecto, la mayoría de las rutas no están restringidas
}
Esto implica que si una ruta no contenía "extension/flujo" (o se accedía a ella con GET), y no era una "extension/confianza", se consideraba segura. La mayoría de las operaciones de negocio caen fuera de estas condiciones, haciendo que el filtro sea ineficaz.
- Debilidades en el Manejo de Solicitudes Ajax
Una vulnerabilidad crítica se encontró en la forma en que el filtro manejaba las solicitudes Ajax frente a las solicitudes regulares:
if (req.getHeader("X-Requested-With") != null
&& req.getHeader("X-Requested-With").equalsIgnoreCase("XMLHttpRequest")) {
// Lógica para solicitudes Ajax: verifica un token de sesión o cookie
String identificadorSesion = UtilCookies.obtenerCookie(req, "identificador_sesion");
if (identificadorSesion == null || identificadorSesion.isEmpty()) {
resp.setHeader("EstadoSesion", "expirada");
return; // Sesión expirada para Ajax
}
cadenaFiltros.doFilter(req, resp);
} else {
// ¡Las solicitudes que no son Ajax se procesan directamente sin validar sesión!
cadenaFiltros.doFilter(req, resp);
}
El problema fatal es que si el encabezado X-Requested-With no está presente (lo cual es el comportamiento predeterminado de un navegador para una solicitud normal), el filtro omite completamente la validación de la sesión. Un atacante puede simplemente no incluir este encabezado en sus solicitudes para eludir todas las comprobaciones de sesión.
- Cadena de Explotación y Evaluación de Riesgos
5.1. Construcción de la Cadena de Ataque
La combinación de estas vulnerabilidades permite a un atacante construir una cadena de ataque efectiva:
- Eludir la lista blanca: Utilizando técnicas de normalización de rutas (ej.,
../). - Falsificar Referer: Estableciendo un encabezado
Refererválido para pasar la verificación (si aplica). - Evitar
esAccesoRestringido: Apuntando a interfaces de negocio comunes que no cumplen las condiciones de restricción. - Saltar validación de sesión para Ajax: Asegurándose de no incluir el encabezado
X-Requested-With.
5.2. Impacto de las Vulnerabilidades
La explotación exitosa de estas fallas puede llevar a consecuencias graves:
- Acceso no autorizado: A cualquier funcionalidad que no esté explícitamente protegida de forma robusta.
- Fuga de datos: Obtención de información sensible del negocio.
- Escalada de privilegios: Ejecución de operaciones con permisos elevados a través de interfaces no autorizadas.
- Manipulación de lógica de negocio: Derivación de flujos de trabajo y controles empresairales normales.
- Recomendaciones de Mitigación y Buenas Prácticas
6.1. Medidas de Reparación Inmediata
-
**Refuerzo de la Validación de Rutas:**Implementar una normalización estricta de las rutas URL antes de cualquier comparación con listas blancas. Es crucial que la normalización ocurra antes de que el filtro evalúe la URL.
// Normalizar la ruta antes de la comparación String rutaNormalizada = java.nio.file.Paths.get(rutaSolicitada).normalize().toString(); // Luego, comparar rutaNormalizada con las entradas de la lista blanca -
**Validación Robusta de Referer:**Validar el
Refererde manera integral, comprobando el dominio y, si es posible, el origen completo, y no solo la presencia del encabezado o un prefijo simple.String referer = req.getHeader("Referer"); if (referer != null) { URI uriReferer = new URI(referer); // Verificar el host del Referer contra una lista de hosts permitidos if (!hostPermitido(uriReferer.getHost())) { // Registrar y denegar el acceso resp.setStatus(HttpServletResponse.SC_FORBIDDEN); return; } } else { // Si Referer es nulo, decidir si se permite o deniega por defecto // Una política estricta denegaría } -
**Eliminar la Lógica de Excepción para Ajax:**Aplicar una lógica de validación de sesión y autenticación uniforme para todas las solicitudes, independientemente de si son Ajax o no. El encabezado
X-Requested-Withno debe ser un criterio para omitir controles de seguridad fundamentales.// Lógica de validación de sesión unificada para todas las solicitudes String tokenSesion = UtilCookies.obtenerCookie(req, "token_seguridad"); if (tokenSesion == null || tokenSesion.isEmpty() || !validarToken(tokenSesion)) { // Manejar la expiración o falta de sesión de forma consistente if ("XMLHttpRequest".equalsIgnoreCase(req.getHeader("X-Requested-With"))) { resp.setHeader("EstadoSesion", "no_autorizado"); } else { resp.sendRedirect(req.getContextPath() + "/relogin.jsp"); } return; } cadenaFiltros.doFilter(req, resp);