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
- Dirty Stream Attack: Turning Android Share Targets Into Attack Vectors
- Cómo compartir archivos de forma segura
- Documentación sobre Cómo solicitar un archivo compartido
- Cómo recuperar información de archivos
- FileProvider
- Salto de directorio
- CWE-73 External Control of Filename or Path