[Android] Remote access: fix a transversal security issue for the download endpoint

Nicolas Pomepuy git at videolan.org
Mon Feb 23 12:42:01 UTC 2026


vlc-android | branch: master | Nicolas Pomepuy <nicolas at videolabs.io> | Mon Sep 29 07:25:26 2025 +0200| [091529a9b18b76e1ff74e473d976bde6d9c0c8a8] | committer: Nicolas Pomepuy

Remote access: fix a transversal security issue for the download endpoint

Fixes #3257

> https://code.videolan.org/videolan/vlc-android/commit/091529a9b18b76e1ff74e473d976bde6d9c0c8a8
---

 .../vlc/remoteaccessserver/RemoteAccessRouting.kt  | 39 +++++++++++++++-------
 1 file changed, 27 insertions(+), 12 deletions(-)

diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt
index 07d1434e1a..91c48b8e29 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt
@@ -1216,20 +1216,35 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) {
             }
             call.respond(HttpStatusCode.NotFound)
         }
-        //Download a file previously prepared
+        // Download a file previously prepared
         get("/download") {
-            call.request.queryParameters["file"]?.let {
-                val dst = File("${RemoteAccessServer.getInstance(appContext).downloadFolder}/$it")
-                call.response.header(
-                        HttpHeaders.ContentDisposition,
-                        ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, dst.toUri().lastPathSegment
-                                ?: "")
-                                .toString()
-                )
-                call.respondFile(dst)
-                dst.delete()
+            val requested = call.request.queryParameters["file"] ?: run {
+                call.respond(HttpStatusCode.BadRequest, "Missing file parameter")
+                return at get
             }
-            call.respond(HttpStatusCode.NotFound)
+
+            val baseDir = File(RemoteAccessServer.getInstance(appContext).downloadFolder).canonicalFile
+            val dstFile = File(baseDir, requested).canonicalFile
+
+            // Enforce that the resolved path stays within the intended download directory
+            if (!dstFile.path.startsWith(baseDir.path + File.separator)) {
+                call.respond(HttpStatusCode.BadRequest, "Invalid file path")
+                return at get
+            }
+
+            // Send as attachment with a safe filename
+            call.response.header(
+                HttpHeaders.ContentDisposition,
+                ContentDisposition.Attachment
+                    .withParameter(ContentDisposition.Parameters.FileName, dstFile.name)
+                    .toString()
+            )
+
+            // Stream the file, then return early to avoid double responses
+            call.respondFile(dstFile)
+            // Optionally delete only if it resides in baseDir
+            runCatching { if (dstFile.exists()) dstFile.delete() }
+            return at get
         }
         //Change the favorite state of a media
         get("/favorite") {



More information about the Android mailing list