Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2026-19264 — CVE-2026-19264 - Traversal de ruta crítico sin autenticación que permite la toma total de la instancia en Postiz (< 2.22.1). Informe técnico: bypass del orden de decodificación, escalada de JWT_SECRET y análisis de la corrección upstream. | Kitploit
Herramientas/GitHubGitHub/darklycn1976/cve-2026-19264
Análisis de VulnerabilidadesAnálisis de CódigoExplotaciónExplotación de Aplicaciones WebSeguridad WebAprendizaje y Educación
GitHubdarklycn1976/cve-2026-19264

CVE-2026-19264

CVE-2026-19264 - Traversal de ruta crítico sin autenticación que permite la toma total de la instancia en Postiz (< 2.22.1). Informe técnico: bypass del orden de decodificación, escalada de JWT_SECRET y análisis de la corrección upstream.

Ver RepositorioSitio web
2hace 1 mesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2026-19264 - Path Traversal no autenticado hasta la toma de control total de la instancia en Postiz

CVE-2026-19264 - Path Traversal no autenticado hasta la toma de control total de la instancia en Postiz

Autor: Krithik Babu P (@DarkLycn1976) Publicado: 2026-08-10 CVE: CVE-2026-19264 Severidad: Crítica - CVSS 4.0 9.3 / CVSS 3.1 9.8 CWE: CWE-22 - Limitación inapropiada de un nombre de ruta a un directorio restringido Afectados: gitroomhq/postiz-app < 2.22.1 Corregido en: v2.22.1


TL;DR

Postiz servía los medios almacenados localmente a través de una ruta que unía los segmentos de ruta proporcionados en la URL al directorio de subida y transmitía el resultado de vuelta al cliente, sin normalización de ruta, sin comprobación de contención y sin autenticación.

La carga útil de traversal obvia devuelve 404, porque Next.js colapsa los segmentos ../ antes del enrutado. Pero los separadores codificados en URL sobreviven a la coincidencia de rutas y se decodifican exactamente una vez más de camino a la llamada al sistema de archivos, restaurando el traversal al otro lado de todas las comprobaciones.

Un atacante no autenticado podía leer cualquier archivo legible por el proceso de la aplicación, incluido su propio entorno, que contiene el secreto de firma de JWT. Debido a que Postiz firma los tokens de sesión con ese secreto y los emite sin una reclamación de expiración, recuperarlo convierte una primitiva de lectura de archivos en una sesión permanente y falsificable como cualquier usuario, incluido un administrador.

Una única petición GET no autenticada hasta la toma de control total de la instancia.


1. Antecedentes

Postiz es una plataforma open source de programación de publicaciones en redes sociales — unas 34.000 estrellas en GitHub en el momento de escribir esto — construida como un frontend de Next.js con un backend de NestJS. Es ampliamente autoalojada por agencias y pequeños equipos para gestionar cuentas de redes sociales conectadas, contenido programado y facturación.

Las implementaciones autoalojadas pueden almacenar los medios subidos localmente en lugar de en un almacenamiento de objetos. Ese comportamiento está controlado por una única variable de entorno:

root@kitploit:~
STORAGE_PROVIDER=local

Este es el valor incluido en .env.example, así que es lo que ejecuta la mayoría de los autoalojadores a menos que configuren deliberadamente S3 o Cloudflare R2.

2. La superficie de ataque

Cuando el almacenamiento local está activo, next.config.js reescribe la ruta pública /uploads/:path* hacia una ruta de API interna:

root@kitploit:~
apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts

Dos propiedades hacen que esta ruta sea interesante antes de que intervenga siquiera un fallo:

  1. No está autenticada. Ningún middleware del frontend la protege. Servir medios públicos es la intención, por lo que no se requiere ninguna sesión.
  2. Es un catch-all. El segmento catch-all opcional [[...path]] significa que cada componente restante de la ruta llega como un array que el manejador es libre de interpretar.

Cuando STORAGE_PROVIDER es cualquier valor distinto de local, la reescritura apunta a /404 y el manejador es inalcanzable. Esa puerta de configuración es lo único que se interpone entre una implementación y este fallo.

3. El código vulnerable

El manejador, antes de v2.22.1:

root@kitploit:~
export const GET = async (request: NextRequest, context) => {
  const { path } = await context.params;
  const filePath =
    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');

  const response  = createReadStream(filePath);
  const fileStats = statSync(filePath);
  // ... stream the file back to the caller
};

Tres defectos en cuatro líneas:

  • Sin normalización. No se llama a path.normalize() ni a path.resolve(). Los segmentos que llegan se concatenan literalmente.
  • Sin comprobación de contención. Nada verifica que el filePath resultante siga viviendo dentro de UPLOAD_DIRECTORY.
  • Concatenación de cadenas, no unión de rutas. + '/' + trata los componentes como texto, no como una ruta con semántica.

El resultado va directamente a createReadStream() y los bytes se transmiten al llamante con un tipo MIME inferido del nombre de archivo. No hay lista blanca de extensiones ni filtro de contenido.

4. Por qué falla la carga útil obvia

El ataque de manual es:

root@kitploit:~
GET /uploads/../../../etc/passwd

En Postiz esto devuelve 404, y ese 404 es la razón completa por la que este fallo sobrevivió hasta ser encontrado.

Next.js normaliza la ruta de la petición durante el enrutado. Los segmentos ../ sin procesar se colapsan antes de que el enrutador decida qué manejador invocar. Para cuando la petición llega al catch-all, el traversal ya ha sido eliminado: o la ruta resuelve a algún sitio sin una ruta coincidente, o resuelve de vuelta dentro de /uploads con los segmentos de punto eliminados.

Para alguien que prueba rápido, ese 404 se lee como "el framework se encarga de esto". Es una defensa genuina y funcional. El problema no es que esté ausente: es dónde en el pipeline se ejecuta.

5. El bypass - un desajuste en el orden de decodificación

La coincidencia de rutas y el manejador de peticiones no realizan el mismo número de pasadas de decodificación de porcentajes.

Si los separadores están codificados en porcentaje, la secuencia no es un separador de ruta durante la coincidencia de rutas. %2e%2e%2f es solo una cadena opaca: texto inerte que el normalizador no tiene razón para tocar. Atraviesa el enrutado intacta, el catch-all la empareja y se decodifica de camino a los params del manejador, donde vuelve a convertirse en ../.

En ese punto se concatena a UPLOAD_DIRECTORY y se entrega a createReadStream(): pasado el enrutado, pasado la normalización, pasado todos los controles que la habrían detenido.

Formas que funcionan:

root@kitploit:~
GET /uploads/%2e%2e%2fsecretdir%2fsecret.txt        → 200, file outside the upload directory
GET /uploads/..%2f..%2f..%2fetc%2fpasswd            → 200
GET /uploads/%2e%2e%2f%2e%2e%2f...%2fetc%2fpasswd   → 200, returned the real /etc/passwd

La doble codificación no funciona: %252e permanece literal a través de la única pasada de decodificación y nunca se convierte en un punto. Exactamente una capa de codificación es el punto óptimo, un recordatorio útil de que «codificar más fuerte» no es una estrategia.

El invariante que debes retener:

Un control que se ejecuta antes de que la decodificación esté completa no está protegiendo el sink.

6. Escalada - de la lectura arbitraria a la toma de control de la instancia

Una primitiva de lectura de archivos es Alta por sí sola. Lo que la convierte en Crítica es lo que alcanza.

Paso 1: lee el entorno. La propia configuración del proceso de Node está en disco en la raíz de la implementación. .env produce, entre otras cosas:

  • JWT_SECRET - la clave de firma de los tokens de sesión
  • DATABASE_URL - credenciales completas de Postgres
  • Secretos OAuth de los proveedores conectados y claves de facturación

Paso 2: forja una sesión. Postiz firma los tokens de sesión con JWT_SECRET usando HS256 mediante jsonwebtoken. De forma crítica, los tokens se emiten sin expiresIn, por lo que un token forjado es válido indefinidamente.

Paso 3: conviértete en cualquiera. El middleware de autenticación vuelve a resolver al usuario desde la base de datos usando la reclamación id. Deliberadamente no confía en una reclamación como isSuperAdmin del token — buen diseño — pero ese endurecimiento es irrelevante una vez que puedes firmar un id arbitrario. Firmar { id: <id de usuario víctima> } produce una sesión indistinguible de un inicio de sesión legítimo:

root@kitploit:~
read .env  →  JWT_SECRET  →  sign({ id: victim })  →  authenticated as victim, forever

Validé esto contra la lógica de verificación real del proyecto con la dependencia real jsonwebtoken: un token firmado con el secreto recuperado fue aceptado, y el mismo token firmado con un secreto incorrecto fue rechazado. El caso de control importa — sin él tienes una suposición, no un hallazgo.

Paso 4: el camino paralelo. DATABASE_URL por sí solo es suficiente para el acceso directo a Postgres: lee todas las cuentas conectadas o cambia directamente un indicador de administrador.

Sin contraseña. Sin acceso previo. Sin interacción del usuario. Una única petición HTTP no autenticada.

7. Impacto y puntuación

root@kitploit:~
CVSS 4.0  9.3  AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS 3.1  9.8  AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

PR:N y UI:N son las dos métricas que hacen el trabajo. La ruta no requiere sesión ni interacción de la víctima: el atacante actúa solo, a través de la red, contra una configuración por defecto.

El único limitador honesto es la puerta de configuración: las implementaciones en S3 o R2 no están expuestas, porque la ruta se reescribe a /404. Eso reduce la población afectada pero no la severidad para cualquiera que esté dentro — y local es el valor por defecto incluido.

8. La corrección

El parche de los mantenedores (7936062) tiene ocho líneas y merece la pena leerlo, porque es correcto de una manera en que estas correcciones a menudo no lo son:

root@kitploit:~
+import { resolve, sep } from 'path';
...
-  const filePath =
-    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');
+  const base = resolve(process.env.UPLOAD_DIRECTORY!);
+  const filePath = resolve(base, (path ?? []).join('/'));
+  // Confine reads to UPLOAD_DIRECTORY. resolve() collapses any `..` segments
+  // (including URL-decoded ones), so this blocks every path-traversal variant.
+  if (filePath !== base && !filePath.startsWith(base + sep)) {
+    return new NextResponse('Not found', { status: 404 });
+  }

Dos cosas que hace bien:

  1. Normaliza en el sink. resolve() colapsa .. después de que la decodificación esté completa, así que no importa cómo se haya colado el traversal a través del enrutado. La comprobación ahora se sitúa donde está el peligro.
  2. Compara contra base + sep, no contra base. Un filePath.startsWith(base) ingenuo aceptaría /app/uploads-evil/x como si estuviera dentro de /app/uploads — un bypass clásico de coincidencia de prefijo. Añadir el separador lo cierra, y la cláusula filePath !== base mantiene el propio directorio como válido.

Esa es la forma correcta para una comprobación de contención: resolver y luego comparar contra la base con un separador final.

9. Cronología de divulgación

Todos los tiempos en UTC, 2026-07-20 salvo que se indique.

HoraEvento
05:55Aviso reportado al equipo de Postiz
07:44Reconocido y verificado por los mantenedores
12:18Corrección commiteada, verificada y publicada
2026-08-07 14:13CVE-2026-19264 asignado por Postiz (CNA)
2026-08-07 14:15Aviso de seguridad de GitHub publicado

Seis horas y veintitrés minutos desde el reporte hasta el parche publicado, en un proyecto open source sin programa de recompensas asociado. He tenido reportes sin tocar durante meses en organizaciones con equipos de seguridad dedicados. Crédito a Enno Gelhaus por la coordinación y a Nevo David por la remediación.

10. Conclusiones

Que un control supere tu prueba no significa que el control esté en el lugar correcto. El 404 era real. Next.js realmente colapsa ../. La defensa simplemente se ejecutaba antes de que la entrada terminara de decodificarse, lo que significaba que estaba protegiendo al enrutador en lugar de a la llamada al sistema de archivos. Cuando encuentres una mitigación, pregunta cuándo se ejecuta en relación con el sink, no solo si existe.

La codificación es una capa, y las capas se pelan a ritmos diferentes. Cada vez que dos componentes de un pipeline de peticiones no se ponen de acuerdo sobre cuántas veces decodificar, la brecha entre ellos es explotable. Los emparejadores de rutas, los middlewares y los manejadores a menudo no se ponen de acuerdo.

Clasifica una primitiva de lectura de archivos por lo que el proceso puede alcanzar, no por la primitiva. «Lectura arbitraria de archivos» suena a divulgación de información. Se convirtió en Crítica porque el entorno era legible, el secreto que contenía firmaba sesiones y esas sesiones nunca expiraban. Sigue la cadena antes de puntuarla.

Los tokens que no expiran convierten una filtración en un compromiso permanente. La divulgación de una clave de firma con tokens de corta duración es un mal día. Sin expiresIn, es irrecuperable sin rotar el secreto, y la mayoría de los operadores nunca sabrán que necesitaban hacerlo.

Ejecuta el caso de control. Verificar que un token firmado con el secreto incorrecto sea rechazado es lo que separa un hallazgo demostrado de uno asumido.

11. Referencias

  • Registro CVE - https://www.cve.org/CVERecord?id=CVE-2026-19264
  • Aviso de seguridad de GitHub - https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-4hgh-5rhf-4qpm
  • Aviso CNA de Postiz (PSA-2026-TH12B7) - https://gadvisory.org/advisories/PSA-2026-TH12B7
  • Commit de la corrección - https://github.com/gitroomhq/postiz-app/commit/7936062
  • Versión parcheada v2.22.1 - https://github.com/gitroomhq/postiz-app/releases/tag/v2.22.1
  • CWE-22 - https://cwe.mitre.org/data/definitions/22.html

Guía de remediación

Si autoalojas Postiz:

  1. Actualiza a v2.22.1 o posterior. Esa es la corrección.
  2. Asume que JWT_SECRET está comprometido si ejecutaste una versión afectada en un host alcanzable públicamente con STORAGE_PROVIDER=local. Rótalo. Como los tokens no llevan expiración, la rotación es la única forma de invalidar cualquiera que haya sido forjado.
  3. Rota las credenciales de DATABASE_URL y cualquier secreto OAuth de proveedores conectados almacenados en el mismo entorno.
  4. Revisa los registros de acceso en busca de peticiones GET a /uploads/ que contengan %2e o %2f.

Investigación realizada de forma independiente y divulgada al proveedor bajo divulgación coordinada. Publicada después de que la corrección se distribuyera y el aviso se hiciera público. No se accedió a sistemas de terceros: toda la validación se realizó contra una instancia local construida a partir del propio código fuente del proyecto.


Licencia

Este informe está licenciado bajo CC BY 4.0: comparte y adapta libremente con atribución. Los extractos de código de gitroomhq/postiz-app se citan para análisis de seguridad y permanecen bajo la licencia de ese proyecto.

Krithik Babu P - @DarkLycn1976

Descargar herramienta