- FFmpeg, qui traite les médias dans les navigateurs et les infrastructures de streaming du monde entier, analyse des entrées complexes et non fiables, ce qui en fait une surface d’attaque sensible du point de vue de la sécurité
- L’agent de sécurité autonome de depthfirst a trouvé 21 zero-day dans environ 1,5 million de lignes de code C optimisé, pour un coût d’environ 1 k$, soit 10 % des 10 k$ dépensés par Anthropic pour Mythos
- Les découvertes couvrent plusieurs composants, dont le demuxer TS, le décodeur VP9 et les chemins de traitement RTP/RTSP/RTMP, certaines vulnérabilités étant restées latentes pendant 15 à 20 ans
- La vulnérabilité du dépaquetiseur AV1 sur RTP a mené à une preuve de concept permettant d’écraser un pointeur de fonction avec un simple paquet RTP de 183 octets, accessible en exécutant seulement
ffmpeg -i rtsp://attacker/stream
- Une validation de sécurité réellement utile exige non pas des alertes théoriques, mais des entrées reproductibles et une confirmation à l’exécution, et l’analyse agentique peut être utilisée directement pour débusquer des vulnérabilités cachées dans de grandes bases de code C
Vue d’ensemble
- FFmpeg est un logiciel largement déployé pour le traitement des médias, notamment dans les navigateurs et l’infrastructure des grandes plateformes de streaming
- Comme il s’agit d’une bibliothèque qui analyse en continu des médias complexes et non fiables, son importance est critique pour la sécurité, et elle constitue une cible majeure pour les attaques zero-click
- Le dépôt FFmpeg se compose d’environ 1,5 million de lignes de code C hautement optimisé et analyse des centaines de formats multimédias complexes
- FFmpeg fait l’objet de fuzzing et d’audits manuels depuis plus de 20 ans, et récemment l’équipe Google Big Sleep a publié 13 vulnérabilités dans FFmpeg
- Anthropic a scanné FFmpeg avec son modèle Mythos et a découvert plusieurs problèmes de sécurité
- Après ces travaux préalables, il est devenu plus difficile de trouver de nouvelles vulnérabilités dans FFmpeg, ce qui en fait une bonne cible pour évaluer les capacités d’un système agentique à scanner en profondeur une grande base de code
L’agent de sécurité de Depthfirst
- Les agents de code et les agents de sécurité peuvent reposer sur le même modèle de base, mais leurs objectifs diffèrent fortement
- Un agent de code se concentre généralement sur l’écriture de code applicatif à partir de tâches confiées par un humain
- Un agent de sécurité remplit un rôle plus étroit et orienté objectif : trouver, sans instruction précise, de vrais problèmes de sécurité exploitables dans un système existant
- Un agent de sécurité doit d’abord comprendre l’architecture de la base de code, les parseurs exposés et les gestionnaires de protocole, ainsi que les points d’entrée des entrées contrôlées par l’attaquant
- Il suit ensuite le flux de données à travers les composants liés au code de surface d’attaque, au lieu de traiter le dépôt comme un simple ensemble plat de fichiers
- Un agent de sécurité réellement pratique a besoin de garde-fous pour éviter d’inventer des conditions manquantes, d’exagérer des bugs théoriques ou de produire un grand volume de faux positifs
- Il doit vérifier si l’attaquant contrôle réellement les bonnes entrées, si le chemin vulnérable est atteignable et si le défaut suspecté est reproductible
- Si nécessaire, il doit identifier ou créer un harness interagissant avec le composant ciblé pour tester concrètement ses hypothèses
- L’agent de sécurité spécialisé de depthfirst analyse le code en profondeur et explore en parallèle plusieurs hypothèses
- Le résultat n’est pas un rapport théorique ni une alerte vague, mais des problèmes de sécurité confirmés à l’exécution avec des entrées concrètes et reproductibles
Résultats des découvertes
- L’agent a découvert au total 21 zero-day, couvrant un périmètre allant du demuxer TS au décodeur VP9
- Le coût total a été d’environ 1 k$, soit environ 10 % du coût engagé par Anthropic pour utiliser Mythos
- Comparaison de coût : {b:1,10}
-
Vulnérabilités auxquelles un CVE a été attribué
- CVE-2026-39210 est un heap buffer overflow dans le demuxer TS, dû à l’absence de vérification des limites de longueur avant la lecture de deux octets, introduit en 2010
- CVE-2026-39211 est un integer overflow introduit en 2010 lors d’un refactoring de swscale, où une formule de coefficient de taille non bornée permettait à des paramètres contrôlés par l’utilisateur de provoquer un agrandissement arbitrairement important
- CVE-2026-39212 est un stack overflow dans
ffmpeg_opt.c, où un fichier preset pouvait déclencher récursivement l’analyse d’options sans limite de profondeur ; il s’agit d’une régression introduite en juillet 2025
- CVE-2026-39213 est un heap buffer overflow sur le chemin d’entrée rawvideo de yuv4mpegenc, dû à l’absence de validation des dimensions par rapport à la taille du paquet ; introduit en 2023
- CVE-2026-39214 est un stack buffer overflow dans l’implémentation SDT d’origine, qui écrivait des entrées de service sans suivre l’espace restant ; introduit en 2003 et resté latent pendant 23 ans
- CVE-2026-39215 est un heap buffer overflow causé par une erreur logique dans
update_mb_info(), qui amenait des appels ultérieurs à écrire 12 octets au-delà du tampon alloué ; introduit en 2012
- CVE-2026-39216 est un heap buffer overflow dans
img2enc.c, causé par le remplacement d’une taille de chroma sûre par une taille non bornée basée sur les dimensions ; introduit en 2012
- CVE-2026-39217 est un heap buffer overflow dans le décodeur VP9, où une fonction de mise à jour de taille refactorisée empêchait la réallocation nécessaire du buffer du thread de tuile ; régression introduite en mars 2025
- CVE-2026-39218 est un heap buffer overflow dans le demuxer DASH, où l’absence de rejet des valeurs de duration négatives rendait négatif l’index du tableau de fragments ; introduit en 2017
-
Vulnérabilités référencées par identifiants internes
- DFVULN-127 est un heap buffer overflow dans
av1_handle_packet() du dépaquetiseur AV1 sur RTP : en ignorant un OBU Temporal Delimiter, la fonction avance la position de sortie de obu_size sans allouer l’espace correspondant, de sorte que l’OBU suivant écrit au-delà de la limite du buffer
- DFVULN-126 est un heap buffer overflow dans
run_legacy_unscaled() du code graphe de swscale, qui traite incorrectement la conversion entrelacée YUV420P→NV12 et écrit 576 octets au-delà du Y-plane cible
- DFVULN-125 est un stack buffer overflow dans
jpeg_create_header() du dépaquetiseur RTP JPEG : lors de la construction de la section des tables de quantification dans un buffer de pile de 1024 octets, AV_WB16 écrit 2 octets au-delà de la fin après un paquet où qtable_len >= 1024
- DFVULN-124 est un heap buffer overflow dans
istg_parse_tile_grid() sur le chemin overlay AVIF : la fonction ne rejette pas une référence dimg avec zéro entrée de tuile, ce qui provoque, après wraparound non signé, une lecture hors limites depuis une allocation heap d’un octet
- DFVULN-123 est un integer overflow dans
latm_parse_packet() du dépaquetiseur RTP LATM : un overflow d’addition signed 32-bit contourne une vérification de limites et amène memcpy à lire environ 1 Go au-delà de la fin du buffer heap
- DFVULN-122 est un heap buffer overflow dans
aac_parse_packet() du dépaquetiseur RTP MPEG-4 : la fonction accepte une longueur AU-headers de 0, alloue 1 octet, puis lit un champ de 4 octets sans vérifier la présence de l’en-tête AU
- DFVULN-121 est un heap buffer underflow dans
read_seek() du demuxer CAF : la valeur de retour -1 de av_index_search_timestamp() n’est pas vérifiée avant d’être utilisée comme index de tableau, ce qui conduit à un accès à index_entries[-1]
- DFVULN-120 est un integer underflow dans
ff_read_riff_info() du demuxer AVI : la fonction utilise size - 4 sans vérifier size >= 4, ce qui, pour un chunk LIST de taille 0, underflowe à environ 4 Go et provoque une allocation d’environ 2 Go
- DFVULN-119 est un heap buffer overflow dans
opt_map() du parseur d’options : un incrément superflu fait interpréter à tort un link-label comme un index de fichier, enregistre un index de flux à -1 et amène une boucle ultérieure à lire avant le tableau AVStream**
- DFVULN-118 est un heap buffer overflow sur le chemin serveur RTSP :
rtsp_read_announce() traite une valeur négative de Content-Length comme valide et permet, avec un ANNOUNCE distant et Content-Length: -1, une écriture hors limites dans sdp[-1]
- DFVULN-117 est un heap buffer overflow dans le client RTMP :
rtmp_calc_swfhash() vérifie seulement in_size < 3 au lieu de in_size < 8, ce qui l’amène à lire 8 octets dans un buffer de 3 octets minimum
- DFVULN-116 est un heap buffer overflow lors du parsing SDP de RTSP :
sdp_parse_line() calcule strlen(control_url) - 1 sur une chaîne vide, ce qui fait wraparound de size_t à SIZE_MAX et entraîne une lecture d’un octet avant le buffer
D’un marqueur de trame ignoré au contrôle du PC
- Parmi les 21 éléments découverts, le heap buffer overflow du dépaquetiseur RTP AV1 est atteignable depuis le réseau sans drapeau spécial
- Il suffit que la victime exécute
ffmpeg -i rtsp://attacker/stream, et un seul paquet de 183 octets peut détourner le flux d’exécution
- Lorsque FFmpeg récupère un flux RTSP, le serveur transmet la vidéo encodée sous forme d’une séquence de paquets RTP
- AV1 organise le bitstream en OBU (Open Bitstream Units), et le format de payload RTP répartit ces OBU sur plusieurs paquets
- Le dépaquetiseur de FFmpeg sert à réassembler ces OBU fragmentés en un elementary stream propre
- Le Temporal Delimiter (TD) est un petit marqueur qui sépare une temporal unit, c’est-à-dire une image, de la suivante
- La spécification impose que le dépaquetiseur « ignore and remove » les TD présents dans le payload
- C’est précisément ce traitement « ignore and remove » qui constitue le point problématique, et l’agent l’a identifié
Cause racine
- Le dépaquetiseur construit progressivement le paquet de sortie, tandis que le curseur
pktpos suit la position du prochain octet à écrire dans pkt->data
pktpos commence à la fin actuelle du paquet
// libavformat/rtpdec_av1.c:199
pktpos = pkt->size;
- Quand le code parcourt les éléments OBU du paquet, chaque octet réellement émis est précédé d’un appel à
av_grow_packet, qui agrandit l’allocation heap de pkt->data
- L’invariant dont dépend toute la routine est que
pktpos ne doit jamais dépasser la taille allouée de pkt->data
- Le code de traitement du Temporal Delimiter viole cet invariant
// 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;
}
- Lorsqu’un TD est ignoré,
pktpos avance de obu_size, valeur déclarée par l’attaquant, mais la mémoire correspondant à cette avance n’est pas allouée
- Le pointeur d’entrée
buf_ptr n’avance pas non plus au-delà des octets du TD
- Si le TD déclare
obu_size = 148, alors pktpos devient 148, alors que pkt->data peut être encore non alloué ou bien beaucoup plus petit que 148 octets
- L’itération suivante réanalyse les octets du TD lui-même, relit l’octet d’en-tête du TD comme nouvelle longueur d’OBU et utilise le payload comme contenu d’un OBU forgé
- Au prochain OBU normal, le paquet n’est agrandi que de la taille de cet 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 l’OBU forgé fait 17 octets,
av_grow_packet alloue un buffer de 81 octets en ajoutant 17 octets et le padding d’entrée de 64 octets de FFmpeg
- L’écriture commence à
pkt->data[148], soit 67 octets au-delà de la fin de l’allocation
- Ce défaut devient un heap buffer overflow où l’attaquant contrôle à la fois l’offset et le contenu
Méthode d’exploitation
- Pour qu’un overflow contrôlable soit utile, il faut une cible intéressante à écraser juste après le buffer, et l’allocator de FFmpeg fournit précisément cette cible
- Quand
av_grow_packet alloue le buffer de données du paquet, il passe par av_buffer_alloc, qui alloue successivement sur le heap le buffer de données, la structure de bookkeeping AVBuffer, puis AVBufferRef
- FFmpeg alloue via
posix_memalign avec un alignement de 64 octets ; un buffer de données de 81 octets occupe donc un chunk de 128 octets, et la structure AVBuffer se retrouve placée juste après
- La structure
AVBuffer contient un pointeur de fonction utilisé comme callback de libération
// 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
...
};
- Par rapport au début du buffer de données, le pointeur
AVBuffer.free se trouve à l’offset 152
- Avec un TD de
obu_size = 148, l’écriture commence à pkt->data[148]
- L’octet d’en-tête du TD
0x10 est réinterprété comme une longueur de 16, et l’en-tête ainsi que le payload de l’OBU forgé sont écrits à partir de l’offset 148
AVBuffer.refcount occupe les offsets 144–147, donc il reste intact puisque le début de l’écriture est à 148, et conserve sa valeur initiale 1
- L’exploit place un troisième OBU forgé dans le payload du TD pour déclencher un appel supplémentaire à
av_grow_packet
- Comme le buffer a été créé par
av_buffer_alloc et non av_buffer_realloc, il n’est pas marqué comme réallouable, et FFmpeg choisit donc un chemin qui alloue un nouveau buffer puis libère l’ancien
// 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 décrémente ensuite le refcount de l’ancien buffer de 1 à 0 et appelle le callback de libération
// libavutil/buffer.c:129
if (atomic_fetch_sub_explicit(&b->refcount, 1, memory_order_acq_rel) == 1) {
b->free(b->opaque, b->data);
}
- À ce moment-là, le pointeur
free écrasé est invoqué, ce qui permet de prendre le contrôle de l’instruction pointer
- En build release, un seul paquet RTP de 183 octets a placé
rip à 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)
Périmètre d’impact et PoC
- Les déploiements où FFmpeg peut être amené à ouvrir une URL RTSP influencée par un attaquant sont exposés à cette vulnérabilité
- Les pipelines d’ingestion média qui récupèrent des URL de flux fournies par l’utilisateur sont concernés
- Les systèmes de surveillance et de vidéosurveillance qui récupèrent des flux RTSP sont concernés
- Les services de transcodage qui traitent des sources AV1-over-RTP distantes sont concernés
- Aucune authentification ni interaction utilisateur supplémentaire au-delà de l’ouverture du flux n’est nécessaire, et aucun flag de ligne de commande inhabituel n’est requis
- La vulnérabilité est déclenchée lors de l’étape RTSP
PLAY normale que ces clients exécutent par conception
- Le code PoC est disponible dans ffmpeg-dfvuln127
1 commentaires
Commentaires sur Hacker News
FFmpeg a un historique particulièrement mauvais en matière de sécurité
Depuis longtemps, chaque fois qu’on lance un fuzzer dessus, il en sort presque sans fin des bugs de corruption mémoire, et il y a même eu un travail d’employés de Google il y a 10 ans : https://security.googleblog.com/2014/01/ffmpeg-and-thousand-...
Donc, même si c’est une démo qui montre les capacités d’un LLM, ce n’est pas vraiment surprenant. Si vous traitez du contenu non fiable ou fourni par des utilisateurs, il ne faut pas exécuter FFmpeg hors d’un sandbox, et le faire revient à accepter un risque déraisonnable
Ils ont parlé d’une vulnérabilité dans un codec extrêmement niche, utilisé au mieux dans un jeu vidéo des années 90, en expliquant que la personne qui l’avait signalée en faisait toute une affaire alors qu’en pratique ce codec n’est presque jamais utilisé
Mais je me suis demandé s’ils ne passaient pas à côté du fait que, si un attaquant peut fournir un fichier vidéo, il peut choisir n’importe quel codec vidéo qu’il veut. Même si les développeurs pensent que ce codec n’est jamais utilisé, tant qu’il reste pris en charge, un attaquant peut s’en servir
Je me demande si quelque chose m’échappe, ou s’il existe une bonne raison pour laquelle la vulnérabilité de ce codec ne serait vraiment pas grave
Jusqu’à récemment, il n’y avait pas de contexte natif virtio GPU, donc il était même impossible de sandboxer un lecteur vidéo sans perdre toute l’accélération matérielle. Du moins, c’était difficile à imposer de l’extérieur ; en interne, on pouvait isoler FFmpeg comme Chromium et le verrouiller fortement avec seccomp
Ce n’est pas un problème propre à FFmpeg. Apple aussi a eu d’innombrables vulnérabilités dans ses décodeurs d’images et de vidéos. Ce domaine est tout simplement très difficile, et FFmpeg en fait plus que quiconque
Je pense que le secteur optimise la mauvaise cible. Avec des outils comme Mythos preview 1 ou GPT-5.5, il est facile de générer des milliers de rapports de bugs écrits par une IA. Ce qui est difficile, c’est de corriger réellement les bugs
Depuis quelques mois, je construis un système qui ouvre des PR au lieu de simplement publier des rapports après avoir trouvé des problèmes de sécurité critiques. Jusqu’ici, le taux d’acceptation tourne autour de 94 %. La plupart des échecs ne venaient pas d’une mauvaise évaluation des vulnérabilités, mais de kill switches propres aux projets ou de mécanismes internes non documentés
Les développeurs semblent généralement préférer cette approche. Les rapports de bugs créent du travail ; de bonnes PR en retirent. Cela paraît évident, mais beaucoup de produits de sécurité s’arrêtent encore au rapport et en restent là
Cela dure depuis les débuts du secteur, et ce n’est que maintenant qu’on commence à disposer des bons outils pour évaluer les dégâts et la fragilité d’ensemble que cela a produits
Ce bug est grave à cause de sa portée. Tout déploiement où FFmpeg pointe vers une URL RTSP influençable par un attaquant est exposé
Cela inclut les pipelines de collecte média qui récupèrent des URL de flux fournies par des utilisateurs, les systèmes de surveillance et de CCTV qui consomment des flux RTSP, ou les services de transcodage qui traitent des sources AV1-over-RTP distantes. En pratique, c’est assez sérieux, et il est même surprenant que cela ait été rendu public. Je pense immédiatement à plusieurs services exploitables dès maintenant
Même si ce n’est peut-être pas aussi grave que ça et que cela ressemble à une pub pour une société de sécurité, cela rappelle qu’il y a des failles de sécurité quelque part dans toutes les applications que vous livrez, et que désormais même un script kiddie peut les trouver 5 minutes après la sortie avec 2 dollars de crédits
Si vous ne faites pas valider votre code par une red team avant la sortie, des hackers le feront à votre place après
Le terme zéro-day est utilisé de façon exagérée. Parmi les vulnérabilités décrites, aucune n’est réellement un zéro-day, mais l’appeler ainsi fait plus sérieux et attire plus de clics
« À ce stade, le pointeur free corrompu est appelé et nous avons le contrôle du pointeur d’instruction » est extrêmement grave
Cela dit, en pratique, il ne semble pas que ce bug à lui seul permette une exécution de code arbitraire à distance. C’est encore plus vrai avec ASLR, et il faudrait qu’il existe quelque part des pages mémoire à la fois inscriptibles et exécutables
system()Il faudrait quand même un autre exploit pour contourner l’ASLR
Ce n’est pas ça que veut dire « zéro-day »
La structure des incitations dans le domaine de la recherche en sécurité est profondément cassée
Ces gens ressemblent à des managers intermédiaires du monde FOSS. Ils sont félicités pour donner encore plus de travail à des bénévoles, et plus ce travail est urgent, plus ils sont félicités. Reconnaître l’impact réel d’un problème ou ses implications pratiques va à l’encontre de leurs incitations
Il est difficile de ne pas les voir comme les éboueurs du secteur logiciel, et j’aimerais qu’on commence désormais à les traiter comme des indésirables. Qu’ils envoient une PR, ou qu’ils se taisent
J’utilise FFmpeg depuis très longtemps, à titre personnel comme dans les services que j’ai créés. Fabrice Bellard est un génie, et les développeurs qui ont amené FFmpeg jusqu’ici ont rendu le monde sensiblement plus riche
Mais lorsqu’il est exécuté sur des entrées non fiables, je ne vois guère de programme qui mérite autant d’être sandboxé que FFmpeg. Il manipule, dans un immense code C, des codecs vidéo et audio complexes notoirement difficiles à rendre totalement corrects
En pratique, ce n’est toutefois pas un si gros problème. On peut exécuter FFmpeg dans une VM ou dans gVisor, et le résultat final est généralement un fichier vidéo qu’on serait prêt à lire dans un navigateur. Il sera alors décodé à nouveau dans un autre sandbox du navigateur, donc c’est de toute façon un problème difficile par nature
Un sandboxing sûr devient facilement une occasion de permettre la copie sans restriction
Il s’agit d’une vulnérabilité où
latm_parse_packet()effectue une addition signée sur 32 bits dans le code de dépaquetisation inverse RTP LATM, provoque un overflow et contourne les vérifications de limitesUne fois encore, une addition non vérifiée crée une vulnérabilité ; pourtant, les langages modernes comme Rust ou Go ne lèvent pas d’exception sur overflow, et les architectures CPU modernes comme RISC-V ne fournissent pas non plus de trap d’overflow. Les anciens langages comme C ou C++ n’ont évidemment pas non plus de vérification d’overflow
C’est absurde. Il est clair qu’on ne peut pas faire confiance aux humains pour écrire un code arithmétique correct
Le comportement par défaut des overflows d’entiers en mode release de Rust est aussi défini : ça wrappe simplement. Cela réduit donc le risque d’aboutir à une vulnérabilité. Bien sûr, c’est différent dès qu’on commence à utiliser unsafe Rust
Rust ne lève pas d’exception sur overflow par défaut, mais on peut écrire
123.checked_add(321). Le code devient alors plus difficile à lire, mais il est sûr vis-à-vis de l’overflowHonnêtement, vu ma manière d’écrire le code, je préférerais quelque chose comme un commentaire de fin de ligne. Par exemple,
var x = y + z; # wrappedIl est très peu probable qu’on mélange, dans une même ligne, arithmétique avec wrap, vérifiée et saturante. Dans Zig, chaque ligne doit pouvoir être compilée telle quelle, sans autre contexte de code ; donc un état du compilateur du type
doing(wrapped) { x + y }n’est pas possible. Les noms de fonction sont beaucoup trop verbeux, et les conversions de type aussi. Un modificateur au niveau de l’instruction pourrait être un bon compromis