[Android] Remote access: consolidate path traversal guards in file routing

Nicolas Pomepuy git at videolan.org
Thu Sep 10 04:41:21 UTC 2026


vlc-android | branch: master | Nicolas Pomepuy <nicolas at videolabs.io> | Wed Sep  9 14:54:45 2026 +0200| [5b60f3301cf318e2ebc1eefe75497f4d37edc3b7] | committer: Nicolas Pomepuy

Remote access: consolidate path traversal guards in file routing

> https://code.videolan.org/videolan/vlc-android/commit/5b60f3301cf318e2ebc1eefe75497f4d37edc3b7
---

 .../routing/RemoteAccessRoutingFile.kt             | 25 +++++++++++-----------
 .../routing/RemoteAccessRoutingUtils.kt            |  9 ++++++++
 2 files changed, 22 insertions(+), 12 deletions(-)

diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingFile.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingFile.kt
index 85956af8d8..89cd88e8c5 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingFile.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingFile.kt
@@ -102,13 +102,14 @@ fun Route.publicFileRouting(appContext: Context, settings: SharedPreferences) {
                     fileDescription = part.value
                 }
                 is PartData.FileItem -> {
-                    File("${AndroidDevices.MediaFolders.EXTERNAL_PUBLIC_DOWNLOAD_DIRECTORY_URI.path}/uploads").mkdirs()
+                    val uploadDir = File("${AndroidDevices.MediaFolders.EXTERNAL_PUBLIC_DOWNLOAD_DIRECTORY_URI.path}/uploads")
+                    uploadDir.mkdirs()
                     fileName = part.originalFileName as String
                     val fileBytes = part.streamProvider().readBytes()
-                    val file = File("${AndroidDevices.MediaFolders.EXTERNAL_PUBLIC_DOWNLOAD_DIRECTORY_URI.path}/uploads/$fileName")
-                    if (file.canonicalFile.parent?.startsWith(File("${AndroidDevices.MediaFolders.EXTERNAL_PUBLIC_DOWNLOAD_DIRECTORY_URI.path}/uploads").absolutePath) != true) {
-                        call.respond(HttpStatusCode.Unauthorized)
-                        throw (IllegalStateException("${file.canonicalFile.parent} is not a valid path"))
+                    val file = File(uploadDir, fileName)
+                    if (!file.isSafelyWithin(uploadDir)) {
+                        call.respond(HttpStatusCode.Forbidden, "Invalid file path")
+                        return at forEachPart
                     }
                     file.writeBytes(fileBytes)
                 }
@@ -134,9 +135,9 @@ fun Route.publicFileRouting(appContext: Context, settings: SharedPreferences) {
                     uploadDir.mkdirs()
                     val fileName = part.originalFileName?.let { File(it).name } ?: "subtitle.srt"
                     val file = File(uploadDir, fileName)
-                    if (!file.canonicalFile.canonicalPath.startsWith(uploadDir.canonicalPath + File.separator)) {
-                        call.respond(HttpStatusCode.Unauthorized)
-                        throw (IllegalStateException("${file.canonicalFile.parent} is not a valid path"))
+                    if (!file.isSafelyWithin(uploadDir)) {
+                        call.respond(HttpStatusCode.Forbidden, "Invalid file path")
+                        return at forEachPart
                     }
                     part.streamProvider().use { input ->
                         file.outputStream().use { output ->
@@ -470,12 +471,12 @@ fun Route.authenticatedFileRouting(appContext: Context, scope: CoroutineScope, s
             return at get
         }
 
-        val baseDir = File(RemoteAccessServer.getInstance(appContext).downloadFolder).canonicalFile
-        val dstFile = File(baseDir, requested).canonicalFile
+        val baseDir = File(RemoteAccessServer.getInstance(appContext).downloadFolder)
+        val dstFile = File(baseDir, requested)
 
         // 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")
+        if (!dstFile.isSafelyWithin(baseDir)) {
+            call.respond(HttpStatusCode.Forbidden, "Invalid file path")
             return at get
         }
 
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingUtils.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingUtils.kt
index 26540f8b70..fd009d2a00 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingUtils.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/routing/RemoteAccessRoutingUtils.kt
@@ -255,3 +255,12 @@ class FormattedDateJsonAdapter : JsonAdapter<Date>() {
 internal suspend fun ApplicationCall.respondJson(text: String, status: HttpStatusCode? = null, configure: OutgoingContent.() -> Unit = {}) {
     respond(TextContent(text, ContentType.Application.Json, status).apply(configure))
 }
+
+/**
+ * Safely checks if a file resides within the target directory, preventing path traversal attacks.
+ */
+internal fun File.isSafelyWithin(parentDir: File): Boolean {
+    val canonicalParent = parentDir.canonicalFile.path
+    val canonicalChild = this.canonicalFile.path
+    return canonicalChild == canonicalParent || canonicalChild.startsWith(canonicalParent + File.separator)
+}



More information about the Android mailing list