[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