Commit 3fb40ab68f for ffmpeg

commit 3fb40ab68f41dfcf3e12c91f6cc54b3e55e0b902
Author: Michael Niedermayer <michael@niedermayer.cc>
Date:   Tue Oct 6 23:40:27 2026 +0200

    avformat/webmdashenc: Escape the metadata written into the manifest

    Fixes: XML injection
    Fixes: wQScoVJMhVGd
    XML injection Replicated through UnModified FFmpeg
    Found during triage of the security report f81gsd1fPSfe

diff --git a/libavformat/webmdashenc.c b/libavformat/webmdashenc.c
index 54fac45cae..63dbe28bb1 100644
--- a/libavformat/webmdashenc.c
+++ b/libavformat/webmdashenc.c
@@ -61,6 +61,7 @@ typedef struct WebMDashMuxContext {
     char *utc_timing_url;
     double time_shift_buffer_depth;
     int minimum_update_period;
+    AVDictionary **escaped_metadata;
 } WebMDashMuxContext;

 static const char *get_codec_name(int codec_id)
@@ -179,7 +180,8 @@ static int write_representation(AVFormatContext *s, AVStream *st, char *id,
     WebMDashMuxContext *w = s->priv_data;
     AVIOContext *pb = s->pb;
     const AVCodecParameters *par = st->codecpar;
-    AVDictionaryEntry *bandwidth = av_dict_get(st->metadata, BANDWIDTH, NULL, 0);
+    AVDictionary *metadata = w->escaped_metadata[st->index];
+    AVDictionaryEntry *bandwidth = av_dict_get(metadata, BANDWIDTH, NULL, 0);
     const char *bandwidth_str;
     avio_printf(pb, "<Representation id=\"%s\"", id);
     if (bandwidth) {
@@ -207,10 +209,10 @@ static int write_representation(AVFormatContext *s, AVStream *st, char *id,
         avio_printf(pb, " startsWithSAP=\"1\"");
         avio_printf(pb, ">");
     } else {
-        AVDictionaryEntry *irange = av_dict_get(st->metadata, INITIALIZATION_RANGE, NULL, 0);
-        AVDictionaryEntry *cues_start = av_dict_get(st->metadata, CUES_START, NULL, 0);
-        AVDictionaryEntry *cues_end = av_dict_get(st->metadata, CUES_END, NULL, 0);
-        AVDictionaryEntry *filename = av_dict_get(st->metadata, FILENAME, NULL, 0);
+        AVDictionaryEntry *irange = av_dict_get(metadata, INITIALIZATION_RANGE, NULL, 0);
+        AVDictionaryEntry *cues_start = av_dict_get(metadata, CUES_START, NULL, 0);
+        AVDictionaryEntry *cues_end = av_dict_get(metadata, CUES_END, NULL, 0);
+        AVDictionaryEntry *filename = av_dict_get(metadata, FILENAME, NULL, 0);
         if (!irange || !cues_start || !cues_end || !filename)
             return AVERROR(EINVAL);

@@ -365,7 +367,7 @@ static int write_adaptation_set(AVFormatContext *s, int as_index)

     if (w->is_live) {
         AVDictionaryEntry *filename =
-            av_dict_get(st->metadata, FILENAME, NULL, 0);
+            av_dict_get(w->escaped_metadata[st->index], FILENAME, NULL, 0);
         char *underscore_pos, *period_pos;
         int ret;
         if (!filename)
@@ -393,7 +395,7 @@ static int write_adaptation_set(AVFormatContext *s, int as_index)
         int ret;
         if (w->is_live) {
             AVDictionaryEntry *filename =
-                av_dict_get(st->metadata, FILENAME, NULL, 0);
+                av_dict_get(w->escaped_metadata[st->index], FILENAME, NULL, 0);
             if (!filename)
                 return AVERROR(EINVAL);
             ret = split_filename(filename->value, &underscore_pos, &period_pos);
@@ -497,6 +499,24 @@ static int webm_dash_manifest_write_header(AVFormatContext *s)
             return AVERROR(EINVAL);
     }

+    w->escaped_metadata = av_calloc(s->nb_streams, sizeof(*w->escaped_metadata));
+    if (!w->escaped_metadata)
+        return AVERROR(ENOMEM);
+    for (i = 0; i < s->nb_streams; i++) {
+        const AVDictionaryEntry *e = NULL;
+        while ((e = av_dict_iterate(s->streams[i]->metadata, e))) {
+            char *value;
+            ret = av_escape(&value, e->value, NULL, AV_ESCAPE_MODE_XML,
+                            AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
+            if (ret < 0)
+                goto fail;
+            ret = av_dict_set(&w->escaped_metadata[i], e->key, value,
+                              AV_DICT_DONT_STRDUP_VAL | AV_DICT_MULTIKEY);
+            if (ret < 0)
+                goto fail;
+        }
+    }
+
     ret = parse_adaptation_sets(s);
     if (ret < 0) {
         goto fail;
@@ -523,6 +543,9 @@ static int webm_dash_manifest_write_header(AVFormatContext *s)
     write_footer(s);
 fail:
     free_adaptation_sets(s);
+    for (i = 0; i < s->nb_streams; i++)
+        av_dict_free(&w->escaped_metadata[i]);
+    av_freep(&w->escaped_metadata);
     return ret < 0 ? ret : 0;
 }