Skip to content

Latest commit

 

History

History
365 lines (284 loc) · 16.5 KB

File metadata and controls

365 lines (284 loc) · 16.5 KB

GStreamer: Multiple Vulnerabilities

Vendors

  • GStreamer

Affected Products

GStreamer gst-plugins-base and gst-plugins-good < 1.26.2

Summary

GStreamer is vulnerable to the following buffer overread issues that could lead to denial of service (crashes) or information disclosure for crafted input files:

  • Heap Out-of-Bounds Read in qtdemux_parse_tree (CVE-2025-47183)
  • Heap Out-of-Bounds Read in qtdemux_parse_trak (CVE-2025-47219)

Additionally, the following vulnerabilities could cause crashes for crafted input files:

  • Null Pointer Dereference in tmplayer_parse_line (CVE-2025-47808)
  • Null Pointer Dereference in subrip_unescape_formatting (CVE-2025-47807)
  • Stack Buffer Overflow in parse_subrip_time (CVE-2025-47806)

Remediation/Mitigation

These issues were patched in the gst-plugins-base and gst-plugins-good 1.26.2 release. Patch files are also available for older releases.

Credit

These issues were found by Shaun Mirani of Atredis Partners.

References

Report Timeline

CVE-2025-47183

  • 2025-04-28: Atredis Partners sent an initial notification to vendor, including a draft advisory.
  • 2025-04-28: Atredis Partners requested a CVE ID from MITRE CNA.
  • 2025-04-28: Vendor acknowledged the report and inquired about a CVE reference. Atredis Partners replied that the CVE ID had been requested and was pending.
  • 2025-04-29: Vendor created a private merge request to address the issue.
  • 2025-05-01: MITRE assigned the vulnerability CVE-2025-47183.
  • 2025-05-02: Atredis Partners notified the vendor of the CVE assignment, which the vendor then acknowledged.
  • 2025-05-29: Vendor merged the fix and released it as part of GStreamer 1.26.2, with a patch available for older versions.
  • 2025-08-06: Atredis Partners publishes advisory ATREDIS-2025-0003

CVE-2025-47219

  • 2025-05-02: Atredis Partners sent an initial notification to vendor, including a draft advisory.
  • 2025-05-02: Atredis Partners requested a CVE from MITRE CNA for this issue. CVE-2025-47219 was assigned.
  • 2025-05-03: Vendor created a private merge request to address the issue for versions 1.26.1, 1.24.x, and earlier versions. For 1.26.2, the issue was separately addressed during a larger refactoring effort.
  • 2025-05-29: Vendor released the fix as part of GStreamer 1.26.2, with a patch available for older versions.
  • 2025-08-06: Atredis Partners publishes advisory ATREDIS-2025-0003

CVE-2025-47806, CVE-2025-47807, and CVE-2025-47808

  • 2025-05-07: Atredis Partners sent an initial notification to vendor, including a draft advisory.
  • 2025-05-08: Vendor created private merge requests to address all three issues. They asked that Atredis Partners request CVEs for them (which had not yet happened as the CVE website was not functioning the day before) and made a technical clarification about one of the issues.
  • 2025-05-08: Atredis Partners requested CVEs from MITRE CNA for all three issues and acknowledged the vendor's comments.
  • 2025-05-10: CVE IDs were assigned to all three issues.
  • 2025-05-29: Vendor merged fixes and released them as part of GStreamer 1.26.2, with patches available for older versions.
  • 2025-08-06: Atredis Partners publishes advisory ATREDIS-2025-0003

Technical Details

Heap Out-of-Bounds Read in qtdemux_parse_tree (CVE-2025-47183)

The isomp4 plugin's qtdemux_parse_tree function may read past the end of a heap buffer while parsing the fields of an MP4 file's mvhd atom. Specifically, the function reads several fields of the atom without sufficient validation of the file's length. If mvhd is the final atom in the file, and the file ends early, the atom will be truncated and an OOB read will occur when assigning creation_time, timescale, and/or duration. The vulnerable source code (permalink) is shown below.

/* we have read the complete moov node now.
 * This function parses all of the relevant info, creates the traks and
 * prepares all data structures for playback
 */
static gboolean
qtdemux_parse_tree (GstQTDemux * qtdemux)
{
  GNode *mvhd;
  GNode *trak;
  GNode *udta;
  GNode *mvex;
  GNode *pssh;
  guint64 creation_time;
  GstDateTime *datetime = NULL;
  gint version;
  GstByteReader mvhd_reader;
  guint32 matrix[9];

  /* make sure we have a usable taglist */
  qtdemux->tag_list = gst_tag_list_make_writable (qtdemux->tag_list);

  mvhd = qtdemux_tree_get_child_by_type_full (qtdemux->moov_node,
      FOURCC_mvhd, &mvhd_reader);
  if (mvhd == NULL) {
    GST_LOG_OBJECT (qtdemux, "No mvhd node found, looking for redirects.");
    return qtdemux_parse_redirects (qtdemux);
  }

  version = QT_UINT8 ((guint8 *) mvhd->data + 8);
  if (version == 1) {
    creation_time = QT_UINT64 ((guint8 *) mvhd->data + 12);
    qtdemux->timescale = QT_UINT32 ((guint8 *) mvhd->data + 28);
    qtdemux->duration = QT_UINT64 ((guint8 *) mvhd->data + 32);
    if (!gst_byte_reader_skip (&mvhd_reader, 4 + 8 + 8 + 4 + 8))
      return FALSE;
  } else if (version == 0) {
    creation_time = QT_UINT32 ((guint8 *) mvhd->data + 12);
    qtdemux->timescale = QT_UINT32 ((guint8 *) mvhd->data + 20);
    qtdemux->duration = QT_UINT32 ((guint8 *) mvhd->data + 24);
    if (!gst_byte_reader_skip (&mvhd_reader, 4 + 4 + 4 + 4 + 4))
      return FALSE;
  } else {
    GST_WARNING_OBJECT (qtdemux, "Unhandled mvhd version %d", version);
    return FALSE;
  }
  // ...

Exploitability Analysis

The worst-case impact of this issue is an info leak that can assist in the exploitation of other vulnerabilities. Data past the end of a heap chunk is copied into the timescale and duration fields of the qtdemux structure. However, the attacker may have difficulty accessing this structure unless the MP4 file parses successfully, which may not be possible in conventional usage of the GStreamer API or command-line utilities. This is because the truncation of the mvhd atom, which is a prerequisite for the OOB read to occur, causes qtdemux to return an error.

While it is unlikely that any real-world code is vulnerable in this manner, it is technically possible to ignore the error from the API and directly access the timescale and duration fields, even if parsing fails. See the example below.

#include <gst/gst.h>
#include "../../gst-plugins-good/gst/isomp4/qtdemux.h"

static void on_pad_added(GstElement *src, GstPad *new_pad, gpointer data) {
    GstElement *sink = (GstElement *)data;
    GstPad *sink_pad = gst_element_get_static_pad(sink, "sink");

    if (!gst_pad_is_linked(sink_pad)) {
        if (gst_pad_link(new_pad, sink_pad) != GST_PAD_LINK_OK) {
            g_printerr("Failed to link demuxer and sink.\n");
        } else {
            g_print("Pad linked successfully.\n");
        }
    }
    gst_object_unref(sink_pad);
}

int main(int argc, char *argv[]) {
    GstElement *pipeline, *source, *demuxer, *decodebin, *sink;
    GstBus *bus;
    GstMessage *msg;
    GstStateChangeReturn ret;

    gst_init(&argc, &argv);

    if (argc != 2) {
        g_printerr("Usage: %s <filename>\n", argv[0]);
        return -1;
    }

    // Create GStreamer elements
    pipeline = gst_pipeline_new("qtdemux-pipeline");
    source = gst_element_factory_make("filesrc", "file-source");
    demuxer = gst_element_factory_make("qtdemux", "qt-demuxer");
    decodebin = gst_element_factory_make("decodebin", "decoder");
    sink = gst_element_factory_make("autovideosink", "video-output");

    if (!pipeline || !source || !demuxer || !decodebin || !sink) {
        if (!pipeline) g_printerr("Failed to create pipeline.\n");
        if (!source) g_printerr("Failed to create filesrc element.\n");
        if (!demuxer) g_printerr("Failed to create qtdemux element.\n");
        if (!decodebin) g_printerr("Failed to create decodebin element.\n");
        if (!sink) g_printerr("Failed to create autovideosink element.\n");
        return -1;
    }

    // Set the file source location
    g_object_set(G_OBJECT(source), "location", argv[1], NULL);

    // Build the pipeline
    gst_bin_add_many(GST_BIN(pipeline), source, demuxer, decodebin, sink, NULL);

    if (!gst_element_link(source, demuxer)) {
        g_printerr("Failed to link source and demuxer.\n");
        return -1;
    }

    // Connect to the pad-added signal for the demuxer
    g_signal_connect(demuxer, "pad-added", G_CALLBACK(on_pad_added), decodebin);

    // Start playing
    ret = gst_element_set_state(pipeline, GST_STATE_PLAYING);
    if (ret == GST_STATE_CHANGE_FAILURE) {
        g_printerr("Failed to set pipeline to PLAYING.\n");
        gst_object_unref(pipeline);
        return -1;
    }

    // Wait for error or EOS
    bus = gst_element_get_bus(pipeline);
    msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS);

    // Parse message
    if (msg != NULL) {
        GError *err;
        gchar *debug_info;

        switch (GST_MESSAGE_TYPE(msg)) {
        case GST_MESSAGE_ERROR:
            gst_message_parse_error(msg, &err, &debug_info);
            g_printerr("Error received from element %s: %s\n",
                       GST_OBJECT_NAME(msg->src), err->message);
            g_printerr("Debugging information: %s\n",
                       debug_info ? debug_info : "none");
            g_clear_error(&err);
            g_free(debug_info);
            break;
        case GST_MESSAGE_EOS:
            g_print("End-Of-Stream reached.\n");
            break;
        default:
            g_printerr("Unexpected message received.\n");
            break;
        }
        gst_message_unref(msg);
    }

    g_print("leaking timescale: 0x%08x\n", ((GstQTDemux *) demuxer)->timescale);
    g_print("leaking duration: 0x%016lx\n", ((GstQTDemux *) demuxer)->duration);

    // Free resources
    gst_object_unref(bus);
    gst_element_set_state(pipeline, GST_STATE_NULL);
    gst_object_unref(pipeline);

    return 0;
}

Compiled as vuln-example-1.0:

GST_DEBUG=9 vuln-example-1.0 ./crash-358d8971f028be24bf2933429a04aa97c833d8ce.mod 2>/dev/null
leaking timescale: 0x6c656e67
leaking duration: 0x7431000000000000

Heap Out-of-Bounds Read in qtdemux_parse_trak (CVE-2025-47219)

The isomp4 plugin's qtdemux_parse_trak function may read past the end of a heap buffer while parsing an MP4 file's stsd atom(s). The vulnerable code (shown below, permalink) iterates from 0 up to the number of stsd entries specified by the file, dereferencing various offsets of the stsd_data buffer, which points to the file data.

However, there is insufficient validation that the file is large enough to hold all expected fields of the specified number of stsd atoms. If the file is too small, an out-of-bounds read on stsd_data will occur at some point during the loop. Atredis Partners observed this to happen when the size or type field is read (assignment to len and fourcc variables below, respectively).

In the code block below, comments added by Atredis Partners are denoted by the prefix "NOTE:".

  // NOTE: number of stsd entries in the file 
  stream->stsd_entries_length = stsd_entry_count = QT_UINT32 (stsd_data + 12);
  /* each stsd entry must contain at least 8 bytes */
  if (stream->stsd_entries_length == 0
      || stream->stsd_entries_length > stsd_len / 8) {
    stream->stsd_entries_length = 0;
    goto corrupt_file;
  }
  stream->stsd_entries = g_new0 (QtDemuxStreamStsdEntry, stsd_entry_count);
  GST_LOG_OBJECT (qtdemux, "stsd len:           %d", stsd_len);
  GST_LOG_OBJECT (qtdemux, "stsd entry count:   %u", stsd_entry_count);
  
  // NOTE: pointer into file data buffer
  stsd_entry_data = stsd_data + 16;
  remaining_stsd_len = stsd_len - 16;
   
  // NOTE: loop in which stsd entries are parsed
  for (stsd_index = 0; stsd_index < stsd_entry_count; stsd_index++) {
    guint32 fourcc;
    gchar *codec = NULL;
    QtDemuxStreamStsdEntry *entry = &stream->stsd_entries[stsd_index];

    /* and that entry should fit within stsd */

    // NOTE: buffer over-read may occur here ("size" field of stsd atom)
    len = QT_UINT32 (stsd_entry_data);
    if (len > remaining_stsd_len)
      goto corrupt_file;
    
    // NOTE: buffer over-read may occur here ("type" field of stsd atom)
    entry->fourcc = fourcc = QT_FOURCC (stsd_entry_data + 4);
    GST_LOG_OBJECT (qtdemux, "stsd type:          %" GST_FOURCC_FORMAT,
        GST_FOURCC_ARGS (entry->fourcc));
    GST_LOG_OBJECT (qtdemux, "stsd type len:      %d", len);
    
    // ...SNIP...

    stsd_entry_data += len;
    remaining_stsd_len -= len;
  }

The OOB read may lead to information disclosure that exposes sensitive data to attackers or facilitates the exploitation of other vulnerabilities, though Atredis Partners has not confirmed this impact.

Null Pointer Dereference in tmplayer_parse_line (CVE-2025-47808)

The tmplayer_parse_line function of the subparse plugin passes a pointer (text_start) that may be NULL as the source buffer to g_string_append, causing a crash for some inputs. The affected code is shown below (permalink).

  if (text_start == NULL || text_start[1] == '\0' ||
      (l == 1 && state->buf->len > 0)) {

    if (GST_CLOCK_TIME_IS_VALID (state->start_time) &&
        state->start_time < ts && line_num > 0) {
      ret = tmplayer_process_buffer (state);
      state->duration = ts - state->start_time;
      /* ..and append current line's text (if there is any) for the next round.
       * We don't have to store ts as pending_start_time, since we deduce the
       * durations from the start times anyway, so as long as the parser just
       * forwards state->start_time by duration after it pushes the line we
       * are about to return it will all be good. */
      g_string_append (state->buf, text_start + 1);

Null Pointer Dereference in subrip_unescape_formatting (CVE-2025-47807)

In subrip_unescape_formatting (subparse plugin), there is a missing check on the return value of g_regex_replace, which may be NULL. The code shown below (permalink) uses the return value as a source for strcpy without verifying that it is not NULL, leading to a crash for some inputs.

tag_regex = g_regex_new (search_pattern, 0, 0, NULL);
res = g_regex_replace (tag_regex, txt, strlen (txt), 0,
    replace_pattern, 0, NULL);

/* res will always be shorter than the input or identical, so this
 * copy is OK */
strcpy (txt, res);

Stack Buffer Overflow in parse_subrip_time (CVE-2025-47806)

In the parse_subrip_time function of the subparse plugin, g_strlcat is repeatedly called in a loop to zero-pad ("0" characters) a timestamp to three digits.

However, the destination pointer (&p[len]) for g_strlcat is set to the position of the timestamp string's null terminator (located at the end of the stack buffer) and incremented with each iteration (see code block below, permalink). This causes zeroes to be written and null-terminated past the bounds of the stack-allocated string (the s buffer).

/* make sure we have exactly three digits after he comma */
p = strchr (s, ',');
if (p == NULL) {
  /* If there isn't a ',' the timestamp is broken */
  /* https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/issues/532#note_100179 */
  GST_WARNING ("failed to parse subrip timestamp string '%s'", s);
  return FALSE;
}

++p;
len = strlen (p);
if (len > 3) {
  p[3] = '\0';
} else
  while (len < 3) {
    g_strlcat (&p[len], "0", 2);
    ++len;
  }