Nom de fichier fourni par ContentProvider non approuvé

Catégorie OWASP : MASVS-CODE : qualité du code

Présentation

FileProvider, une sous-classe de ContentProvider, vise à fournir une méthode sécurisée pour une application ("application de serveur") afin de partager des fichiers avec une autre application ("application cliente"). Toutefois, si l'application cliente ne gère pas correctement le nom de fichier fourni par l'application de serveur, une application de serveur contrôlée par un pirate informatique peut mettre en œuvre son propre FileProvider malveillant en vue d'écraser les fichiers présents dans l'espace de stockage propre à l'application cliente.

Impact

Si un pirate informatique parvient à écraser les fichiers d'une application, cela peut entraîner l'exécution de code malveillant (en écrasant le code de l'application) ou sinon, permettre la modification du comportement de l'application (par exemple, en écrasant les préférences partagées de l'application ou d'autres fichiers de configuration).

Stratégies d'atténuation

Ne pas approuver les entrées utilisateur

Lors des appels du système de fichiers, il est préférable de travailler sans entrée utilisateur et de générer un nom de fichier unique lors de l'écriture du fichier reçu dans l'espace de stockage.

En d'autres termes, lorsque l'application cliente écrit le fichier reçu dans l'espace de stockage, elle doit ignorer le nom de fichier fourni par l'application de serveur et utiliser à la place son propre identifiant unique généré en interne comme nom de fichier.

Cet exemple s'appuie sur le code trouvé à l'adresse 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;
}

Nettoyer les noms de fichiers fournis

Nettoyez le nom de fichier fourni lors de l'écriture du fichier reçu dans l'espace de stockage.

Cette atténuation est moins souhaitable que l'atténuation précédente, car il peut être difficile de gérer tous les cas potentiels. Néanmoins, s'il est impossible de générer un nom de fichier unique, l'application cliente doit nettoyer le nom de fichier fourni. Le nettoyage comprend :

  • la suppression des caractères de traversée de répertoire dans le nom de fichier
  • le choix de l'URL canonique afin de vérifier l'absence de traversées de répertoire

Cet exemple de code s'appuie sur les conseils fournis concernant la récupération des informations sur un fichier :

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
}

Contributeurs : Dimitrios Valsamaras et Michael Peck de Microsoft Threat Intelligence

Ressources