CVE-2018-6360: Improper Input Validation (CWE-20)
value: mpv through 0.28.0 allows remote attackers to execute arbitrary code via a crafted web site, because it reads HTML documents containing VIDEO elements, and accepts arbitrary URLs in a src attribute without a protocol whitelist in player/lua/ytdl_hook.lua. For example, an av://lavfi:ladspa=file= URL signifies that the product should call dlopen on a shared object file located at an arbitrary local pathname. The issue exists because the product does not consider that youtube-dl can provide a potentially unsafe URL.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 8.8
Published: 2018-01-28T02:29Z
Repository: https://github.com/mpv-player/mpv
Language: Lua
Files changed: player/lua/ytdl_hook.lua
Commit date: 2018-01-26 01:47:43 +0000
Commit message: ytdl_hook: whitelist protocols from urls retrieved from youtube-dl

Not very clean since there's a lot of potential unsafe urls that youtube-dl
can give us, depending on whether it's a single url, split tracks,
playlists, segmented dash, etc.

Diff: diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua
index dd96ecc01d..b480c21625 100644
--- a/player/lua/ytdl_hook.lua
+++ b/player/lua/ytdl_hook.lua
@@ -14,10 +14,22 @@ local ytdl = {
     blacklisted = {}
 }
 
 local chapter_list = {}
 
+function Set (t)
+    local set = {}
+    for _, v in pairs(t) do set[v] = true end
+    return set
+end
+
+local safe_protos = Set {
+    "http", "https", "ftp", "ftps",
+    "rtmp", "rtmps", "rtmpe", "rtmpt", "rtmpts", "rtmpte",
+    "data"
+}
+
 local function exec(args)
     local ret = utils.subprocess({args = args})
     return ret.status, ret.stdout, ret
 end
 
@@ -181,10 +193,13 @@ local function edl_track_joined(fragments, protocol, is_live, base)
         end
     end
 
     for i = offset, #fragments do
         local fragment = fragments[i]
+        if not url_is_safe(join_url(base, fragment)) then
+            return nil
+        end
         table.insert(parts, edl_escape(join_url(base, fragment)))
         if fragment.duration then
             parts[#parts] =
                 parts[#parts] .. ",length="..fragment.duration
         end
@@ -206,20 +221,31 @@ local function proto_is_dash(json)
     local reqfmts = json["requested_formats"]
     return (reqfmts ~= nil and reqfmts[1]["protocol"] == "http_dash_segments")
            or json["protocol"] == "http_dash_segments"
 end
 
+local function url_is_safe(url)
+    local proto = type(url) == "string" and url:match("^(.+)://") or nil
+    local safe = proto and safe_protos[proto]
+    if not safe then
+        msg.error(("Ignoring potentially unsafe url: '%s'"):format(url))
+    end
+    return safe
+end
+
 local function add_single_video(json)
     local streamurl = ""
     local max_bitrate = 0
 
     if has_native_dash_demuxer() and proto_is_dash(json) then
         local mpd_url = json["requested_formats"][1]["manifest_url"] or
             json["manifest_url"]
         if not mpd_url then
             msg.error("No manifest URL found in JSON data.")
             return
+        elseif not url_is_safe(mpd_url) then
+            return
         end
 
         streamurl = mpd_url
 
         if json.requested_formats then
@@ -236,18 +262,22 @@ local function add_single_video(json)
         for _, track in pairs(json.requested_formats) do
             local edl_track = nil
             edl_track = edl_track_joined(track.fragments,
                 track.protocol, json.is_live,
                 track.fragment_base_url)
+            local url = edl_track or track.url
+            if not url_is_safe(url) then
+                return
+            end
             if track.acodec and track.acodec ~= "none" then
                 -- audio track
                 mp.commandv("audio-add",
-                    edl_track or track.url, "auto",
+                    url, "auto",
                     track.format_note or "")
             elseif track.vcodec and track.vcodec ~= "none" then
                 -- video track
-                streamurl = edl_track or track.url
+                streamurl = url
             end
         end
 
     elseif not (json.url == nil) then
         local edl_track = nil
@@ -262,11 +292,17 @@ local function add_single_video(json)
         return
     end
 
     msg.debug("streamurl: " .. streamurl)
 
-    mp.set_property("stream-open-filename", streamurl:gsub("^data:", "data://", 1))
+    streamurl = streamurl:gsub("^data:", "data://", 1)
+
+    if not url_is_safe(streamurl) then
+        return
+    end
+
+    mp.set_property("stream-open-filename", streamurl)
 
     mp.set_property("file-local-options/force-media-title", json.title)
 
     -- set hls-bitrate for dash track selection
     if max_bitrate > 0 and
@@ -524,18 +560,22 @@ mp.add_hook(o.try_ytdl_first and "on_load" or "on_load_fail", 10, function ()
                     --]]
                     if entry["webpage_url"] and not self_redirecting_url then
                         site = entry["webpage_url"]
                     end
 
-                    if not (site:find("https?://") == 1) then
-                        site = "ytdl://" .. site
+                    -- links with only youtube id as returned by --flat-playlist
+                    if not site:find("://") then
+                        table.insert(playlist, "ytdl://" .. site)
+                    elseif url_is_safe(site) then
+                        table.insert(playlist, site)
                     end
-                    table.insert(playlist, site)
 
                 end
 
-                mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
+                if #playlist > 0 then
+                    mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
+                end
             end
 
         else -- probably a video
             add_single_video(json)
         end

Vulnerable code:                     edl_track or track.url, "auto",
                streamurl = edl_track or track.url
    mp.set_property("stream-open-filename", streamurl:gsub("^data:", "data://", 1))
                    if not (site:find("https?://") == 1) then
                        site = "ytdl://" .. site
                    table.insert(playlist, site)
                mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
Fixed code: function Set (t)
    local set = {}
    for _, v in pairs(t) do set[v] = true end
    return set
end

local safe_protos = Set {
    "http", "https", "ftp", "ftps",
    "rtmp", "rtmps", "rtmpe", "rtmpt", "rtmpts", "rtmpte",
    "data"
}

        if not url_is_safe(join_url(base, fragment)) then
            return nil
        end
local function url_is_safe(url)
    local proto = type(url) == "string" and url:match("^(.+)://") or nil
    local safe = proto and safe_protos[proto]
    if not safe then
        msg.error(("Ignoring potentially unsafe url: '%s'"):format(url))
    end
    return safe
end

        elseif not url_is_safe(mpd_url) then
            return
            local url = edl_track or track.url
            if not url_is_safe(url) then
                return
            end
                    url, "auto",
                streamurl = url
    streamurl = streamurl:gsub("^data:", "data://", 1)

    if not url_is_safe(streamurl) then
        return
    end

    mp.set_property("stream-open-filename", streamurl)
                    -- links with only youtube id as returned by --flat-playlist
                    if not site:find("://") then
                        table.insert(playlist, "ytdl://" .. site)
                    elseif url_is_safe(site) then
                        table.insert(playlist, site)
                if #playlist > 0 then
                    mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
                end