diff --git a/app/src/main/java/sh/sar/isodroid/isodrive/IsoDriveManager.kt b/app/src/main/java/sh/sar/isodroid/isodrive/IsoDriveManager.kt index 2e4de53..1a932a4 100644 --- a/app/src/main/java/sh/sar/isodroid/isodrive/IsoDriveManager.kt +++ b/app/src/main/java/sh/sar/isodroid/isodrive/IsoDriveManager.kt @@ -150,8 +150,9 @@ class IsoDriveManager(private val context: Context) { ) } - // Validate file exists - val fileCheck = RootManager.executeCommand("test -f \"$isoPath\" && echo exists") + // Validate file exists using shell-safe escaping + val safePath = RootManager.shellEscape(isoPath) + val fileCheck = RootManager.executeCommand("test -f $safePath && echo exists") if (!fileCheck.success || !fileCheck.output.contains("exists")) { return@withContext MountResult( success = false, @@ -167,9 +168,9 @@ class IsoDriveManager(private val context: Context) { ) } - // Build command + // Build command with safe path escaping val args = options.toCommandArgs().joinToString(" ") - val command = "$binaryPath \"$isoPath\" $args" + val command = "$binaryPath $safePath $args" val result = RootManager.executeCommand(command) diff --git a/app/src/main/java/sh/sar/isodroid/ui/screens/SettingsScreen.kt b/app/src/main/java/sh/sar/isodroid/ui/screens/SettingsScreen.kt index 85c4ea1..2bda423 100644 --- a/app/src/main/java/sh/sar/isodroid/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/sh/sar/isodroid/ui/screens/SettingsScreen.kt @@ -456,9 +456,10 @@ private fun DirectoryBrowserDialog( fun loadContents(path: String) { scope.launch { isLoading = true + val safePath = RootManager.shellEscape(path) // Load directories val dirResult = RootManager.executeCommand( - "find \"$path\" -maxdepth 1 -mindepth 1 -type d 2>/dev/null" + "find $safePath -maxdepth 1 -mindepth 1 -type d 2>/dev/null" ) val directories = if (dirResult.success && dirResult.output.isNotBlank()) { dirResult.output.lines() @@ -467,8 +468,9 @@ private fun DirectoryBrowserDialog( .filter { !it.substringAfterLast("/").startsWith(".") } .map { dirPath -> // Check if this directory was created by the app (has .isodroiddir marker) + val safeDirPath = RootManager.shellEscape(dirPath) val markerCheck = RootManager.executeCommand( - "test -f \"$dirPath/.isodroiddir\" && echo 'yes' || echo 'no'" + "test -f $safeDirPath/.isodroiddir && echo 'yes' || echo 'no'" ) val isDeletable = markerCheck.output.trim() == "yes" BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable) @@ -479,7 +481,7 @@ private fun DirectoryBrowserDialog( // Load ISO/IMG files val fileResult = RootManager.executeCommand( - "find \"$path\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null" + "find $safePath -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null" ) val files = if (fileResult.success && fileResult.output.isNotBlank()) { fileResult.output.lines() @@ -501,9 +503,10 @@ private fun DirectoryBrowserDialog( if (trimmedName.isEmpty()) return@launch val newPath = "$currentPath/$trimmedName" - RootManager.executeCommand("mkdir -p \"$newPath\"") + val safeNewPath = RootManager.shellEscape(newPath) + RootManager.executeCommand("mkdir -p $safeNewPath") // Create marker file to indicate this folder was created by the app - RootManager.executeCommand("touch \"$newPath/.isodroiddir\"") + RootManager.executeCommand("touch $safeNewPath/.isodroiddir") // Auto-navigate into the new folder currentPath = newPath } @@ -511,7 +514,8 @@ private fun DirectoryBrowserDialog( fun deleteFolder(path: String) { scope.launch { - RootManager.executeCommand("rm -rf \"$path\"") + val safePath = RootManager.shellEscape(path) + RootManager.executeCommand("rm -rf $safePath") loadContents(currentPath) } } diff --git a/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt b/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt index 8f4da37..43e9bde 100644 --- a/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt +++ b/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt @@ -205,9 +205,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { // Create directory if it doesn't exist if (!directory.exists()) { - RootManager.executeCommand("mkdir -p \"$currentPath\"") + val safePath = RootManager.shellEscape(currentPath) + RootManager.executeCommand("mkdir -p $safePath") // Create marker file to indicate this folder was created by the app - RootManager.executeCommand("touch \"$currentPath/.isodroiddir\"") + RootManager.executeCommand("touch $safePath/.isodroiddir") } // Try multiple methods to list files @@ -220,8 +221,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { private suspend fun loadFilesViaFind(currentPath: String): List? { // Use find command - more reliable for getting full paths + val safePath = RootManager.shellEscape(currentPath) val result = RootManager.executeCommand( - "find \"$currentPath\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null" + "find $safePath -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null" ) if (!result.success || result.output.isBlank()) return null @@ -231,8 +233,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { .mapNotNull { filePath -> val file = File(filePath.trim()) val name = file.name - // Get file size via stat - val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null") + // Get file size via stat with safe escaping + val safeFilePath = RootManager.shellEscape(filePath.trim()) + val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null") val size = sizeResult.output.trim().toLongOrNull() ?: 0L IsoFile( path = filePath.trim(), @@ -246,8 +249,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { private suspend fun loadFilesViaLs(currentPath: String): List? { // Simple ls command - just get filenames + val safePath = RootManager.shellEscape(currentPath) val result = RootManager.executeCommand( - "ls \"$currentPath\" 2>/dev/null" + "ls $safePath 2>/dev/null" ) if (!result.success || result.output.isBlank()) return null @@ -259,8 +263,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } .mapNotNull { name -> val filePath = "$currentPath/$name" - // Get file size via stat - val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null") + // Get file size via stat with safe escaping + val safeFilePath = RootManager.shellEscape(filePath) + val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null") val size = sizeResult.output.trim().toLongOrNull() ?: 0L IsoFile( path = filePath, @@ -444,13 +449,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val blockSize = 1024 * 1024L // 1MB blocks val totalBlocks = totalBytes / blockSize var writtenBlocks = 0L + val safeFilePath = RootManager.shellEscape(filePath) // Create file with dd in background, checking for cancellation val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { try { // First, create the file with truncate to reserve space indication val createResult = RootManager.executeCommand( - "dd if=/dev/zero of=\"$filePath\" bs=1M count=0 seek=$totalBlocks 2>/dev/null" + "dd if=/dev/zero of=$safeFilePath bs=1M count=0 seek=$totalBlocks 2>/dev/null" ) if (!createResult.success) { @@ -461,18 +467,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { while (writtenBlocks < totalBlocks) { if (CreateImgEventBus.isCancelRequested()) { // Clean up partial file - RootManager.executeCommand("rm -f \"$filePath\"") + RootManager.executeCommand("rm -f $safeFilePath") return@withContext false } // Write a chunk (up to 64MB at a time for efficiency) val chunksToWrite = minOf(64, totalBlocks - writtenBlocks) val chunkResult = RootManager.executeCommand( - "dd if=/dev/zero of=\"$filePath\" bs=1M count=$chunksToWrite seek=$writtenBlocks conv=notrunc 2>/dev/null" + "dd if=/dev/zero of=$safeFilePath bs=1M count=$chunksToWrite seek=$writtenBlocks conv=notrunc 2>/dev/null" ) if (!chunkResult.success) { - RootManager.executeCommand("rm -f \"$filePath\"") + RootManager.executeCommand("rm -f $safeFilePath") return@withContext false } @@ -484,7 +490,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { true } catch (e: Exception) { - RootManager.executeCommand("rm -f \"$filePath\"") + RootManager.executeCommand("rm -f $safeFilePath") false } } @@ -508,14 +514,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val oldPath = file.path val newPath = "${oldPath.substringBeforeLast("/")}/$newName" + // Use shell-safe escaping to prevent command injection + val safeOldPath = RootManager.shellEscape(oldPath) + val safeNewPath = RootManager.shellEscape(newPath) + // Check if new file already exists - val checkResult = RootManager.executeCommand("test -f \"$newPath\" && echo exists") + val checkResult = RootManager.executeCommand("test -f $safeNewPath && echo exists") if (checkResult.output.trim() == "exists") { _uiState.update { it.copy(errorMessage = "File already exists: $newName") } return@launch } - val result = RootManager.executeCommand("mv \"$oldPath\" \"$newPath\"") + val result = RootManager.executeCommand("mv $safeOldPath $safeNewPath") if (result.success) { _uiState.update { it.copy(successMessage = "Renamed to $newName") } loadFiles() @@ -527,7 +537,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun deleteFile(file: IsoFile) { viewModelScope.launch { - val result = RootManager.executeCommand("rm -f \"${file.path}\"") + val safePath = RootManager.shellEscape(file.path) + val result = RootManager.executeCommand("rm -f $safePath") if (result.success) { _uiState.update { it.copy(successMessage = "Deleted ${file.name}") } loadFiles()