attempt to prevent shell escape

This commit is contained in:
2026-03-13 00:25:09 +05:00
parent 4b22871ab4
commit f3dc0b65d6
3 changed files with 42 additions and 26 deletions
@@ -150,8 +150,9 @@ class IsoDriveManager(private val context: Context) {
) )
} }
// Validate file exists // Validate file exists using shell-safe escaping
val fileCheck = RootManager.executeCommand("test -f \"$isoPath\" && echo exists") val safePath = RootManager.shellEscape(isoPath)
val fileCheck = RootManager.executeCommand("test -f $safePath && echo exists")
if (!fileCheck.success || !fileCheck.output.contains("exists")) { if (!fileCheck.success || !fileCheck.output.contains("exists")) {
return@withContext MountResult( return@withContext MountResult(
success = false, 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 args = options.toCommandArgs().joinToString(" ")
val command = "$binaryPath \"$isoPath\" $args" val command = "$binaryPath $safePath $args"
val result = RootManager.executeCommand(command) val result = RootManager.executeCommand(command)
@@ -456,9 +456,10 @@ private fun DirectoryBrowserDialog(
fun loadContents(path: String) { fun loadContents(path: String) {
scope.launch { scope.launch {
isLoading = true isLoading = true
val safePath = RootManager.shellEscape(path)
// Load directories // Load directories
val dirResult = RootManager.executeCommand( 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()) { val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
dirResult.output.lines() dirResult.output.lines()
@@ -467,8 +468,9 @@ private fun DirectoryBrowserDialog(
.filter { !it.substringAfterLast("/").startsWith(".") } .filter { !it.substringAfterLast("/").startsWith(".") }
.map { dirPath -> .map { dirPath ->
// Check if this directory was created by the app (has .isodroiddir marker) // Check if this directory was created by the app (has .isodroiddir marker)
val safeDirPath = RootManager.shellEscape(dirPath)
val markerCheck = RootManager.executeCommand( 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" val isDeletable = markerCheck.output.trim() == "yes"
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable) BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
@@ -479,7 +481,7 @@ private fun DirectoryBrowserDialog(
// Load ISO/IMG files // Load ISO/IMG files
val fileResult = RootManager.executeCommand( 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()) { val files = if (fileResult.success && fileResult.output.isNotBlank()) {
fileResult.output.lines() fileResult.output.lines()
@@ -501,9 +503,10 @@ private fun DirectoryBrowserDialog(
if (trimmedName.isEmpty()) return@launch if (trimmedName.isEmpty()) return@launch
val newPath = "$currentPath/$trimmedName" 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 // 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 // Auto-navigate into the new folder
currentPath = newPath currentPath = newPath
} }
@@ -511,7 +514,8 @@ private fun DirectoryBrowserDialog(
fun deleteFolder(path: String) { fun deleteFolder(path: String) {
scope.launch { scope.launch {
RootManager.executeCommand("rm -rf \"$path\"") val safePath = RootManager.shellEscape(path)
RootManager.executeCommand("rm -rf $safePath")
loadContents(currentPath) loadContents(currentPath)
} }
} }
@@ -205,9 +205,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!directory.exists()) { 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 // 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 // Try multiple methods to list files
@@ -220,8 +221,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private suspend fun loadFilesViaFind(currentPath: String): List<IsoFile>? { private suspend fun loadFilesViaFind(currentPath: String): List<IsoFile>? {
// Use find command - more reliable for getting full paths // Use find command - more reliable for getting full paths
val safePath = RootManager.shellEscape(currentPath)
val result = RootManager.executeCommand( 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 if (!result.success || result.output.isBlank()) return null
@@ -231,8 +233,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
.mapNotNull { filePath -> .mapNotNull { filePath ->
val file = File(filePath.trim()) val file = File(filePath.trim())
val name = file.name val name = file.name
// Get file size via stat // Get file size via stat with safe escaping
val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null") val safeFilePath = RootManager.shellEscape(filePath.trim())
val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null")
val size = sizeResult.output.trim().toLongOrNull() ?: 0L val size = sizeResult.output.trim().toLongOrNull() ?: 0L
IsoFile( IsoFile(
path = filePath.trim(), path = filePath.trim(),
@@ -246,8 +249,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private suspend fun loadFilesViaLs(currentPath: String): List<IsoFile>? { private suspend fun loadFilesViaLs(currentPath: String): List<IsoFile>? {
// Simple ls command - just get filenames // Simple ls command - just get filenames
val safePath = RootManager.shellEscape(currentPath)
val result = RootManager.executeCommand( val result = RootManager.executeCommand(
"ls \"$currentPath\" 2>/dev/null" "ls $safePath 2>/dev/null"
) )
if (!result.success || result.output.isBlank()) return null if (!result.success || result.output.isBlank()) return null
@@ -259,8 +263,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
.mapNotNull { name -> .mapNotNull { name ->
val filePath = "$currentPath/$name" val filePath = "$currentPath/$name"
// Get file size via stat // Get file size via stat with safe escaping
val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null") val safeFilePath = RootManager.shellEscape(filePath)
val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null")
val size = sizeResult.output.trim().toLongOrNull() ?: 0L val size = sizeResult.output.trim().toLongOrNull() ?: 0L
IsoFile( IsoFile(
path = filePath, path = filePath,
@@ -444,13 +449,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val blockSize = 1024 * 1024L // 1MB blocks val blockSize = 1024 * 1024L // 1MB blocks
val totalBlocks = totalBytes / blockSize val totalBlocks = totalBytes / blockSize
var writtenBlocks = 0L var writtenBlocks = 0L
val safeFilePath = RootManager.shellEscape(filePath)
// Create file with dd in background, checking for cancellation // Create file with dd in background, checking for cancellation
val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
try { try {
// First, create the file with truncate to reserve space indication // First, create the file with truncate to reserve space indication
val createResult = RootManager.executeCommand( 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) { if (!createResult.success) {
@@ -461,18 +467,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
while (writtenBlocks < totalBlocks) { while (writtenBlocks < totalBlocks) {
if (CreateImgEventBus.isCancelRequested()) { if (CreateImgEventBus.isCancelRequested()) {
// Clean up partial file // Clean up partial file
RootManager.executeCommand("rm -f \"$filePath\"") RootManager.executeCommand("rm -f $safeFilePath")
return@withContext false return@withContext false
} }
// Write a chunk (up to 64MB at a time for efficiency) // Write a chunk (up to 64MB at a time for efficiency)
val chunksToWrite = minOf(64, totalBlocks - writtenBlocks) val chunksToWrite = minOf(64, totalBlocks - writtenBlocks)
val chunkResult = RootManager.executeCommand( 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) { if (!chunkResult.success) {
RootManager.executeCommand("rm -f \"$filePath\"") RootManager.executeCommand("rm -f $safeFilePath")
return@withContext false return@withContext false
} }
@@ -484,7 +490,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
true true
} catch (e: Exception) { } catch (e: Exception) {
RootManager.executeCommand("rm -f \"$filePath\"") RootManager.executeCommand("rm -f $safeFilePath")
false false
} }
} }
@@ -508,14 +514,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val oldPath = file.path val oldPath = file.path
val newPath = "${oldPath.substringBeforeLast("/")}/$newName" 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 // 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") { if (checkResult.output.trim() == "exists") {
_uiState.update { it.copy(errorMessage = "File already exists: $newName") } _uiState.update { it.copy(errorMessage = "File already exists: $newName") }
return@launch return@launch
} }
val result = RootManager.executeCommand("mv \"$oldPath\" \"$newPath\"") val result = RootManager.executeCommand("mv $safeOldPath $safeNewPath")
if (result.success) { if (result.success) {
_uiState.update { it.copy(successMessage = "Renamed to $newName") } _uiState.update { it.copy(successMessage = "Renamed to $newName") }
loadFiles() loadFiles()
@@ -527,7 +537,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun deleteFile(file: IsoFile) { fun deleteFile(file: IsoFile) {
viewModelScope.launch { 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) { if (result.success) {
_uiState.update { it.copy(successMessage = "Deleted ${file.name}") } _uiState.update { it.copy(successMessage = "Deleted ${file.name}") }
loadFiles() loadFiles()