Contents
Introducción
Proteger y servir archivos privados en WordPress tras un login es un requisito común para sitios con contenido reservado (descargas pagadas, recursos para miembros, documentos internos). Este artículo explica en detalle varias estrategias seguras y prácticas para servir archivos solo a usuarios autorizados usando PHP y la integración con WordPress, cubriendo desde la arquitectura de almacenamiento hasta implementaciones concretas, gestión de rangos para streaming, y optimizaciones con X-Sendfile/X-Accel-Redirect.
Resumen de enfoques
- Archivos fuera del webroot: almacenar archivos fuera de la carpeta pública y servirlos con un script PHP que valide permisos.
- Carpeta protegida con .htaccess y enrutamiento a PHP: bloquear acceso directo y reenviar peticiones a un controlador PHP que valide login.
- Redirección interna del servidor (X-Sendfile / X-Accel-Redirect): el script PHP valida, luego delega la entrega al servidor para mejor rendimiento.
- URLs firmadas / temporales: generar enlaces con firma/HMAC o nonces para descargar sin mantener sesión activa permanente.
Requisitos previos y consideraciones
- Acceso al sistema de archivos del servidor (para mover archivos fuera del public_html si es posible).
- Permisos para editar .htaccess (Apache) o ficheros de configuración de Nginx si se usa X-Accel-Redirect.
- Conocer si el hosting soporta X-Sendfile (Apache) o X-Accel-Redirect (Nginx) para optimizar descargas grandes.
- Validación estricta de rutas y sanidad del parámetro de archivo para evitar path traversal.
Arquitectura recomendada
- Almacenar archivos protegidos fuera del webroot: e.g. /var/www/protected_files o wp-content/uploads-protected pero con reglas que impidan acceso directo.
- Tener un controlador PHP (p. ej. download.php o endpoint dentro de un plugin) que cargue WP (require_once ABSPATH . wp-load.php), valide is_user_logged_in() y capacidades adicionales si es necesario.
- Después de validar, servir el archivo con encabezados apropiados o delegar al servidor con X-Sendfile/X-Accel-Redirect.
- Utilizar tokens o firmas para enlaces públicos temporales si se necesita que usuarios sin sesión legítima descarguen con caducidad.
Protección básica: .htaccess para bloquear acceso directo
Si mantienes los archivos dentro de la jerarquía WordPress, impide acceso directo con una regla en la carpeta protegida (o en la raíz). Ejemplo de .htaccess para denegar acceso y canalizar a PHP:
# Bloquear acceso directo a /wp-content/uploads-protected/ Order deny,allow Deny from all # Alternativamente, redirigir peticiones hacia un controlador: RewriteEngine On RewriteRule ^wp-content/uploads-protected/(.) /wp-content/plugins/my-protect-plugin/download.php?file=1 [L,QSA]
Controlador PHP: ejemplo básico
Ejemplo de archivo download.php que valida sesión WP y sirve el archivo desde una ruta protegida fuera del webroot. Este ejemplo incluye saneamiento básico y envío en bloques para ficheros grandes.
Uso de X-Sendfile (Apache) o X-Accel-Redirect (Nginx)
Para archivos grandes y para descargar de forma más eficiente, conviene delegar la entrega al servidor web. El flujo es el mismo (validar en PHP) pero en vez de leer el archivo se envía una cabecera especial:
- Apache con módulo mod_xsendfile: header(X-Sendfile: /ruta/completa/al/archivo)
- Nginx con internal location: header(X-Accel-Redirect: /internal_protected/archivo)
Ejemplo X-Sendfile (PHP)
// Después de validar permisos y localizar file_path... if ( function_exists(apache_get_modules) in_array(mod_xsendfile, apache_get_modules()) ) { header(X-Sendfile: . file_path) header(Content-Type: . content_type) header(Content-Disposition: attachment filename= . basename . ) exit }
Ejemplo X-Accel-Redirect (Nginx)
Configuración Nginx (en nginx.conf) — exponer una location interna que apunte a la carpeta real:
# nginx.conf (fragmento) location /internal_protected/ { internal alias /var/www/protected_files/ }
En PHP, devolver la cabecera:
// Asumiendo relative = subcarpeta/archivo.pdf header(X-Accel-Redirect: /internal_protected/ . relative) header(Content-Type: . content_type) header(Content-Disposition: attachment filename= . basename . ) exit
Soporte de Range Requests (seek en vídeo/audio)
Si sirves archivos de medios y necesitas permitir reanudación o seek, debes manejar la cabecera HTTP Range y responder con 206 Partial Content cuando corresponda. A continuación un esquema simplificado — adaptar y testar en producción:
// Manejo muy básico de Range (resumido): size = filesize(file_path) start = 0 length = size end = size - 1 if (isset(_SERVER[HTTP_RANGE])) { // Parsear Range: bytes=START-END if (preg_match(/bytes=(d)-(d)/, _SERVER[HTTP_RANGE], matches)) { if (matches[1] !== ) { start = intval(matches[1]) } if (matches[2] !== ) { end = intval(matches[2]) } length = end - start 1 header(HTTP/1.1 206 Partial Content) header(Content-Range: bytes start-end/size) } } header(Accept-Ranges: bytes) header(Content-Length: . length) fh = fopen(file_path, rb) fseek(fh, start) buffer = 8192 while (!feof(fh) (pos = ftell(fh)) <= end) { if (pos buffer > end) { buffer = end - pos 1 } echo fread(fh, buffer) @ob_flush() flush() } fclose(fh) exit
Generar enlaces firmados o temporales
Para permitir descargas desde enlaces que expiran (por ejemplo, usuarios que comparten URL), se puede usar HMAC con una clave secreta o nonces de WordPress. Idea general:
- Al generar un enlace, incluir parámetros expires y signature = HMAC(secret, fileexpires).
- En el controlador, recalcular HMAC y verificar que expires > time() y que la firma coincide.
// Generar enlace (ejecutado en código de WP) file = mi.pdf expires = time() 3600 // 1 hora secret = WP_HOME . . DB_SECRET_KEY // ejemplo mejor usar una constante segura sig = hash_hmac(sha256, file . . expires, secret) link = add_query_arg(array(file => file, expires => expires, sig => sig), site_url(/wp-content/plugins/my-protect-plugin/download.php)) echo esc_url(link) // Validar en download.php: if ( empty(_GET[sig]) empty(_GET[expires]) intval(_GET[expires]) < time() ) { status_header(403) exit(Expired or invalid) } expected = hash_hmac(sha256, requested . . intval(_GET[expires]), secret) if ( !hash_equals(expected, _GET[sig]) ) { status_header(403) exit(Invalid signature) }
Integración como endpoint WP (mejor práctica para plugin)
En lugar de un archivo suelto, implementa un endpoint mediante add_rewrite_rule o usando admin-ajax/o REST API. Ejemplo rápido con add_rewrite_rule template_redirect:
// En el plugin bootstrap: add_action(init, function() { add_rewrite_rule(^protected-download/([^/] )/?,index.php?protected_file=matches[1],top) add_rewrite_tag(%protected_file%,([^] )) }) // Manejo en template_redirect add_action(template_redirect, function() { file = get_query_var(protected_file) if (!file) return // aquí incluir lógica de validación y entrega (como download.php) // No olvidar exit() después de servir para evitar render de WP. })
Seguridad y mejores prácticas
- Nunca confíes en el input del usuario: sanear, usar realpath y comprobar siempre que el archivo está dentro del árbol esperado.
- Comprobar permisos adicionales: si dependes de roles, verificar current_user_can() o metadatos de la suscripción.
- Evitar exponer rutas reales: si usas firmas HMAC, no pases la ruta absoluta en la URL.
- Logs y auditoría: registra descargas importantes o accesos fallidos para detectar abuso.
- Rate limiting: evitar abuse por scripts que intentan descargar masivamente.
- Cabeceras de seguridad: aplicar Cache-Control/Pragma según el caso si los archivos no deben cachearse en proxies, establecer no-store/no-cache.
- Protección contra disclosure de PHP ejecutable: no almacenar archivos subidos en áreas donde puedan ejecutarse server-side.
Consideraciones de rendimiento
- Para muchos usuarios o archivos grandes, X-Sendfile/X-Accel-Redirect reduce memoria y uso CPU de PHP.
- Usar chunked reads para no agotar memoria de PHP si no se dispone de X-Sendfile.
- Si se usan URLs firmadas, considerar una duración prudente (ej. 5-60 minutos) para minimizar riesgo de compartidos.
- Si el acceso concurrente crece, usar CDN con soporte para URLs firmadas o políticas de autenticación del backend.
Ejemplo completo mínimo (recap)
1) Mover archivos a /var/www/protected_files/
2) Proteger carpeta en .htaccess o configurar Nginx internal.
3) Implementar download.php que carga WP, valida is_user_logged_in() y/o firma, y sirve con X-Accel/X-Sendfile o readfile por bloques.
Conclusión técnica
Servir archivos protegidos en WordPress requiere combinar control de acceso (WP), protección del sistema de archivos y entrega eficiente (X-Sendfile/X-Accel si es posible). La implementación puede ser un archivo PHP standalone, un endpoint dentro de un plugin o una REST route. Los puntos críticos son la validación de rutas, la comprobación de permisos y el manejo eficiente de ficheros grandes. Siguiendo las técnicas mostradas se logra una solución segura y escalable para la mayoría de escenarios.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |