[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