¿Qué es la Inyección LDAP y por qué aparece en mi análisis de seguridad?

Si has ejecutado un escáner de vulnerabilidades en tu sitio WordPress y te ha reportado cientos de puntos vulnerables de «Inyección LDAP» relacionados con el parámetro de búsqueda ?s=, no entres en pánico. En este artículo te explico qué significa realmente este reporte y cómo solucionarlo de forma definitiva.

#El problema: 310 vulnerabilidades de Inyección LDAP detectadas

Al analizar un sitio WordPress, el escáner reportó algo como esto:

text
Criticidad: Alta
Descripción: Se encontraron 310 puntos vulnerables donde la aplicación 
no sanitiza correctamente la entrada del usuario antes de usarla 
en una consulta LDAP.

Ejemplos:
- https://tusitio.es/?s=%2A%29%29%00nosuchvalue
- https://tusitio.es/author/admin/?%2A%29%29%00nosuchvalue

El patrón %2A%29%29%00 decodificado es *)) seguido de un carácter NUL (nulo), que son caracteres típicos usados para explotar vulnerabilidades en sistemas que utilizan LDAP (Lightweight Directory Access Protocol).

#¿Es realmente una vulnerabilidad crítica?

Probablemente NO, y aquí está el por qué:

  • WordPress no usa LDAP de forma nativa – El sistema de búsqueda de WordPress utiliza consultas MySQL, no LDAP.
  • El escáner detecta la aceptación de caracteres, no una vulnerabilidad real – Si la URL acepta esos caracteres especiales sin bloquearlos, el escáner lo marca como «vulnerable».
  • Sin plugins de directorios activos, no hay LDAP – Solo sitios que integran WordPress con Active Directory o servicios de directorio LDAP serían vulnerables.

Sin embargo, es una buena práctica bloquear estos patrones por varias razones:

  • Elimina los falsos positivos del informe de seguridad
  • Previene futuros problemas si alguna vez instalas un plugin que use LDAP
  • Mejora la higiene general de seguridad de tu sitio
  • Bloquea intentos de ataques automatizados

#La solución: Bloquear patrones de inyección LDAP en WordPress

Vamos a implementar una solución completa que:

  1. Bloquee caracteres típicos de inyección LDAP en todas las URLs
  2. Sanitice específicamente el parámetro de búsqueda ?s=
  3. Mejore el escapado en las plantillas del tema

#Paso 1: Añadir la función de protección en functions.php

Abre el archivo functions.php de tu tema (preferiblemente un tema hijo) y añade el siguiente código:

text
<?php
/**
 * ==========================================
 * PROTECCIÓN CONTRA INYECCIÓN LDAP
 * ==========================================
 * Bloquea caracteres típicos de inyección LDAP:
 * - Paréntesis: ( )
 * - Asteriscos: *
 * - Caracteres NUL: \x00 o %00
 * - Backslash: \
 * - Pipe: |
 * - Ampersand: &
 */
function bloquear_inyeccion_ldap() {
    // Solo procesar si hay parámetros GET
    if (empty($_GET)) {
        return;
    }
    
    // Obtener la query string completa
    $query_string = isset($_SERVER['QUERY_STRING']) ? urldecode($_SERVER['QUERY_STRING']) : '';
    $request_uri = isset($_SERVER['REQUEST_URI']) ? urldecode($_SERVER['REQUEST_URI']) : '';
    
    // Patrones de inyección LDAP a bloquear
    $ldap_patterns = array(
        '/\x00/',                    // Caracteres NUL
        '/%00/',                     // NUL codificado
        '/\*\)/',                    // *) - patrón típico de LDAP injection
        '/\(\|/',                    // (| - patrón OR en LDAP
        '/\(\&/',                // (& - patrón AND en LDAP
        '/\)\)/',                    // )) - cierre de múltiples filtros
        '/\(\*/',                    // (* - wildcard en filtro
        '/\\\\[0-9a-fA-F]{2}/',      // Escape hex de LDAP
        '/uid=\*/',                  // Enumeración de usuarios LDAP
        '/cn=\*/',                   // Common name wildcard
        '/objectclass=\*/',          // Object class enumeration
    );
    
    // Verificar query string y URI
    foreach ($ldap_patterns as $pattern) {
        if (preg_match($pattern, $query_string) || preg_match($pattern, $request_uri)) {
            // Log del intento (opcional)
            // error_log('LDAP Injection bloqueado desde IP: ' . $_SERVER['REMOTE_ADDR']);
            
            // Responder con error 400 Bad Request
            status_header(400);
            wp_die(
                '<h1>Solicitud no válida</h1>' .
                '<p>La solicitud contiene caracteres no permitidos.</p>' .
                '<p><a href="' . esc_url(home_url('/')) . '">Volver al inicio</a></p>',
                'Solicitud no válida',
                array(
                    'response' => 400,
                    'back_link' => true
                )
            );
        }
    }
    
    // Verificar específicamente el parámetro de búsqueda 's'
    if (isset($_GET['s'])) {
        $search = $_GET['s'];
        
        // Caracteres prohibidos en búsqueda
        $forbidden_chars = array('*', '(', ')', '\\', chr(0), '|', '&', '!');
        
        foreach ($forbidden_chars as $char) {
            if (strpos($search, $char) !== false) {
                // Limpiar la búsqueda
                $clean_search = str_replace($forbidden_chars, '', $search);
                $clean_search = trim($clean_search);
                
                if (!empty($clean_search)) {
                    // Redirigir a búsqueda limpia
                    wp_safe_redirect(add_query_arg('s', urlencode($clean_search), home_url('/')));
                    exit;
                } else {
                    // Redirigir al inicio si no queda nada
                    wp_safe_redirect(home_url('/'));
                    exit;
                }
            }
        }
    }
}
// Ejecutar muy temprano, antes que cualquier otra cosa
add_action('init', 'bloquear_inyeccion_ldap', 0);

#Paso 2: Sanitizar las consultas de búsqueda

Añade también esta función para sanitizar las búsquedas antes de que lleguen a la base de datos:

text
<?php
/**
 * Limpiar query de búsqueda para prevenir inyección SQL y LDAP
 */
function sanitizar_busqueda_wordpress($query) {
    if ($query->is_search && !is_admin()) {
        // Obtener el término de búsqueda
        $search_term = get_search_query();
        
        // Eliminar etiquetas HTML
        $search_term = strip_tags($search_term);
        
        // Eliminar caracteres de inyección LDAP
        $search_term = str_replace(
            array('*', '(', ')', '\\', '|', '&', '!', chr(0)), 
            '', 
            $search_term
        );
        
        // Solo permitir letras, números, espacios, guiones y puntos
        $search_term = preg_replace('/[^\p{L}\p{N}\s\-_\.]/u', '', $search_term);
        
        // Limitar longitud (máximo 100 caracteres)
        $search_term = substr($search_term, 0, 100);
        
        // Normalizar espacios múltiples
        $search_term = preg_replace('/\s+/', ' ', $search_term);
        $search_term = trim($search_term);
        
        // Actualizar query
        if (!empty($search_term)) {
            $query->set('s', $search_term);
        } else {
            // Cancelar búsqueda si queda vacía
            $query->set('s', '');
            $query->is_search = false;
        }
    }
    return $query;
}
add_filter('pre_get_posts', 'sanitizar_busqueda_wordpress');

#Paso 3: Rate Limiting para búsquedas (opcional pero recomendado)

Para prevenir ataques de fuerza bruta en el buscador, añade un limitador de velocidad:

text
<?php
/**
 * Rate limiting para búsquedas (prevenir spam y ataques)
 */
function limitar_busquedas_por_ip() {
    if (!is_search()) {
        return;
    }
    
    // Obtener IP del usuario
    $user_ip = $_SERVER['REMOTE_ADDR'];
    $transient_key = 'search_limit_' . md5($user_ip);
    
    // Obtener número de búsquedas realizadas
    $search_count = get_transient($transient_key);
    
    // Límite: 10 búsquedas por minuto
    $max_searches = 10;
    $time_window = 60; // segundos
    
    if ($search_count === false) {
        set_transient($transient_key, 1, $time_window);
    } elseif ($search_count >= $max_searches) {
        wp_die(
            '<h1>Demasiadas búsquedas</h1>' .
            '<p>Has excedido el límite. Espera un momento antes de intentarlo.</p>' .
            '<p><a href="' . esc_url(home_url('/')) . '">Volver al inicio</a></p>',
            'Límite de búsquedas excedido',
            array('response' => 429, 'back_link' => true)
        );
    } else {
        set_transient($transient_key, $search_count + 1, $time_window);
    }
}
add_action('template_redirect', 'limitar_busquedas_por_ip', 1);

#Probando la solución

Una vez implementado el código, puedes probar que funciona correctamente:

#Prueba 1: URL con patrón de inyección LDAP

Intenta acceder a:
https://tusitio.es/?s=*))test

Resultado esperado: Deberías ver una página de error «Solicitud no válida» con código 400, o ser redirigido a una búsqueda limpia.

#Prueba 2: Búsqueda normal

Intenta acceder a:
https://tusitio.es/?s=wordpress tutorial

Resultado esperado: La búsqueda debería funcionar normalmente.

#Prueba 3: Volver a ejecutar el escáner

Ejecuta nuevamente el análisis de seguridad. Los 310 puntos de inyección LDAP deberían desaparecer.

#¿Por qué funciona esta solución?

Capa de protección Qué hace
Hook ‘init’ con prioridad 0 Se ejecuta antes que cualquier otra cosa en WordPress, bloqueando URLs maliciosas de raíz
Detección de patrones regex Identifica secuencias típicas de inyección LDAP como *), (|, (&, ))
Sanitización de parámetro ‘s’ Limpia específicamente el campo de búsqueda eliminando caracteres peligrosos
Redirección inteligente En lugar de mostrar error, redirige a búsqueda limpia cuando es posible
Rate limiting Previene ataques automatizados limitando búsquedas por IP

#Mejoras adicionales en el formulario de búsqueda

Si usas un tema personalizado, también puedes añadir validación HTML5 en el formulario de búsqueda (searchform.php):

text
<input 
    type="search" 
    name="s" 
    placeholder="Buscar..." 
    value="<?php echo esc_attr(get_search_query()); ?>" 
    maxlength="100"
    pattern="[^*()\|&!]*"
    title="No se permiten caracteres especiales como *, (, ), |, &"
    required 
/>

Esto añade:

  • esc_attr() – Escapado de seguridad para el valor
  • maxlength="100" – Límite de caracteres
  • pattern – Validación del lado del cliente que bloquea caracteres prohibidos

Recuerda siempre hacer una copia de seguridad antes de modificar el archivo functions.php y probar los cambios en un entorno de desarrollo antes de aplicarlos en producción.