Se confía incorrectamente en el nombre de archivo proporcionado por ContentProvider

Categoría de OWASP: MASVS-CODE: Calidad de código

Descripción general

El objetivo de FileProvider, una subclase de ContentProvider, consiste en proporcionar un método seguro para una aplicación ("aplicación de servidor") para compartir archivos con otra aplicación ("aplicación cliente"). Sin embargo, si la aplicación cliente no maneja adecuadamente el nombre de archivo que proporciona la aplicación del servidor, si un atacante controla esta última, podría implementar su propio FileProvider malicioso para reemplazar archivos en el almacenamiento específico de la aplicación cliente.

Impacto

Si un atacante reemplaza los archivos de una aplicación, esto puede llevar a la ejecución de código malicioso (al reemplazar el código de la aplicación) o permitir que se modifique de otro modo el comportamiento de la aplicación (por ejemplo, a través del reemplazo de las preferencias compartidas de la aplicación u otros archivos de configuración).

Mitigaciones

No se confía en la entrada del usuario

Es preferible trabajar sin la entrada del usuario cuando se usan llamadas al sistema de archivos. Para ello, se genera un nombre de archivo único cuando se escribe el archivo recibido al almacenamiento.

En otras palabras: cuando la aplicación cliente escribe el archivo recibido al almacenamiento, debe ignorar el nombre de archivo proporcionado por la aplicación del servidor y, en su lugar, usar su propio identificador único generado de forma interna como nombre del archivo.

Este ejemplo se basa en el código que se encuentra en https://developer.android.com/training/secure-file-sharing/request-file:

Kotlin

// Code in
// https://developer.android.com/training/secure-file-sharing/request-file#OpenFile
// used to obtain file descriptor (fd)

try {
    val inputStream = FileInputStream(fd)
    val tempFile = File.createTempFile("temp", null, cacheDir)
    val outputStream = FileOutputStream(tempFile)
    val buf = ByteArray(1024)
    var len: Int
    len = inputStream.read(buf)
    while (len > 0) {
        if (len != -1) {
            outputStream.write(buf, 0, len)
            len = inputStream.read(buf)
        }
    }
    inputStream.close()
    outputStream.close()
} catch (e: IOException) {
    e.printStackTrace()
    Log.e("MainActivity", "File copy error.")
    return
}

Java

// Code in
// https://developer.android.com/training/secure-file-sharing/request-file#OpenFile
// used to obtain file descriptor (fd)

FileInputStream inputStream = new FileInputStream(fd);

// Create a temporary file
File tempFile = File.createTempFile("temp", null, getCacheDir());

// Copy the contents of the file to the temporary file
try {
    OutputStream outputStream = new FileOutputStream(tempFile))
    byte[] buffer = new byte[1024];
    int length;
    while ((length = inputStream.read(buffer)) > 0) {
        outputStream.write(buffer, 0, length);
    }
} catch (IOException e) {
    e.printStackTrace();
    Log.e("MainActivity", "File copy error.");
    return;
}

Limpia los nombres de archivos proporcionados

Limpia el nombre de archivo proporcionado cuando escribas el archivo recibido al almacenamiento.

Esta mitigación es menos conveniente que la anterior, ya que puede ser difícil manejar todos los casos potenciales. Sin embargo, si no resulta práctico generar un nombre de archivo único, la aplicación cliente debe limpiar el nombre de archivo proporcionado. La limpieza incluye lo siguiente:

  • Limpiar los caracteres de salto de directorio en el nombre de archivo
  • Realizar una canonicalización para confirmar que no hay saltos de directorio

Este código de ejemplo se basa en la guía sobre cómo recuperar información del archivo:

Kotlin

protected fun sanitizeFilename(displayName: String): String {
    val badCharacters = arrayOf("..", "/")
    val segments = displayName.split("/")
    var fileName = segments[segments.size - 1]
    for (suspString in badCharacters) {
        fileName = fileName.replace(suspString, "_")
    }
    return fileName
}

val displayName = returnCursor.getString(nameIndex)
val fileName = sanitizeFilename(displayName)
val filePath = File(context.filesDir, fileName).path

// saferOpenFile defined in Android developer documentation
val outputFile = saferOpenFile(filePath, context.filesDir.canonicalPath)

// fd obtained using Requesting a shared file from Android developer
// documentation

val inputStream = FileInputStream(fd)

// Copy the contents of the file to the new file
try {
    val outputStream = FileOutputStream(outputFile)
    val buffer = ByteArray(1024)
    var length: Int
    while (inputStream.read(buffer).also { length = it } > 0) {
        outputStream.write(buffer, 0, length)
    }
} catch (e: IOException) {
    // Handle exception
}

Java

protected String sanitizeFilename(String displayName) {
    String[] badCharacters = new String[] { "..", "/" };
    String[] segments = displayName.split("/");
    String fileName = segments[segments.length - 1];
    for (String suspString : badCharacters) {
        fileName = fileName.replace(suspString, "_");
    }
    return fileName;
}

String displayName = returnCursor.getString(nameIndex);
String fileName = sanitizeFilename(displayName);
String filePath = new File(context.getFilesDir(), fileName).getPath();

// saferOpenFile defined in Android developer documentation

File outputFile = saferOpenFile(filePath,
    context.getFilesDir().getCanonicalPath());

// fd obtained using Requesting a shared file from Android developer
// documentation

FileInputStream inputStream = new FileInputStream(fd);

// Copy the contents of the file to the new file
try {
    OutputStream outputStream = new FileOutputStream(outputFile))
    byte[] buffer = new byte[1024];
    int length;
    while ((length = inputStream.read(buffer)) > 0) {
        outputStream.write(buffer, 0, length);
    }
} catch (IOException e) {
    // Handle exception
}

Colaboradores: Dimitrios Valsamaras y Michael Peck de Microsoft Threat Intelligence

Recursos