Tester les chemins et limites d’extraction ZIP sur iOS

Tester les chemins et limites d’extraction ZIP sur iOS

Tester les chemins et limites d’extraction ZIP sur iOS

Un utilisateur importe une archive ZIP. L’application annonce que l’opération a réussi, mais des fichiers ont été écrits hors du répertoire d’import. Le problème vient généralement non pas de l’algorithme de compression, mais du nom des entrées, directement concaténé au chemin de destination. Lorsque vous portez ce flux sur un Mac dans le cloud, testez d’abord les règles de validation des chemins, puis intégrez l’extracteur du projet. N’utilisez pas de véritables données utilisateur pour construire des échantillons malveillants et ne laissez pas les tests écrire dans le répertoire du code source.

Définir d’abord le périmètre de l’import

Pour cet import, n’acceptez que les fichiers ordinaires et les répertoires. Refusez les liens symboliques et les types inconnus. Créez un répertoire de staging distinct pour chaque import et ne déplacez son contenu vers le répertoire de l’application qu’après une validation complète. Si une seule entrée échoue, supprimez toutes les données de staging, sans conserver les éléments « déjà importés avec succès ».

Fixez trois quotas : mille entrées au maximum, vingt mégaoctets par fichier et cent mégaoctets au total après décompression. Ces valeurs illustrent une politique de contrôle ; ce ne sont pas des limites du système. Adaptez-les aux besoins de votre application. Le contrôle préalable s’appuie sur les tailles déclarées dans le répertoire de l’archive ; lors de l’écriture, cumulez les octets réellement produits. Aucun de ces deux contrôles ne remplace l’autre.

La sandbox ne constitue pas une frontière de sécurité pour l’extracteur. Même sans pouvoir écrire hors du conteneur de l’application, une entrée malveillante peut encore écraser une base de données ou un fichier de configuration à l’intérieur de ce conteneur.

Écrire un contrôle préalable des chemins exécutable

Enregistrez le code ci-dessous dans zip-path-check.swift, puis exécutez xcrun swift zip-path-check.swift sur le Mac dans le cloud. Ce script valide uniquement les noms des entrées ; il ne lit pas le ZIP. Dans le projet, les noms et les types d’entrées fournis par la bibliothèque d’extraction doivent passer par la même politique de contrôle. Le succès de cette commande ne signifie pas que l’ensemble du processus d’import est sécurisé.

import Foundation

enum PathError: Error {
    case invalid
}

func safeComponents(_ name: String) throws -> [String] {
    guard !name.isEmpty,
          !name.hasPrefix("/"),
          !name.contains("\\"),
          !name.contains(":"),
          !name.unicodeScalars.contains(where: {
              CharacterSet.controlCharacters.contains($0)
          }) else {
        throw PathError.invalid
    }

    let body = name.hasSuffix("/") ? String(name.dropLast()) : name
    let parts = body.split(
        separator: "/",
        omittingEmptySubsequences: false
    ).map(String.init)

    guard !parts.isEmpty,
          parts.allSatisfy({
              !$0.isEmpty && $0 != "." && $0 != ".."
          }) else {
        throw PathError.invalid
    }
    return parts
}

let cases: [(String, Bool)] = [
    ("docs/readme.txt", true),
    ("docs/", true),
    ("report..txt", true),
    ("../outside.txt", false),
    ("docs/../../outside.txt", false),
    ("/tmp/outside.txt", false),
    ("docs//readme.txt", false),
    ("docs/./readme.txt", false),
    ("docs\\..\\outside.txt", false),
    ("C:/outside.txt", false),
    ("docs/\u{0000}bad.txt", false)
]

for (name, expected) in cases {
    let accepted = (try? safeComponents(name)) != nil
    precondition(accepted == expected, "Path policy mismatch")
}
print("Path checks passed")

Ces règles d’import sont volontairement strictes : elles n’acceptent que les chemins relatifs dont les composants sont séparés par des barres obliques, avec une seule barre oblique finale autorisée pour les répertoires. Les deux-points et les caractères de contrôle sont également refusés afin d’éviter les différences d’interprétation entre systèmes de fichiers. Si votre application a réellement besoin de ces caractères, ajoutez des cas de test explicites plutôt que d’assouplir tous les contrôles.

Contrôler les liens et les destinations en conflit

Même si le chemin est valide, le type de l’entrée doit encore être vérifié. N’extrayez pas un lien pour ensuite le suivre lors de l’écriture de l’entrée suivante. Ne vous fiez pas non plus à l’extension pour déterminer le type. Consultez le type et les attributs exposés par la bibliothèque d’extraction ; refusez tout type que vous ne pouvez pas identifier de manière fiable.

Construisez un index des chemins de destination, composant par composant. Au-delà des doublons exacts, bloquez aussi les conflits parent-enfant, par exemple lorsqu’une archive demande d’écrire d’abord un fichier docs, puis docs/a.txt. L’ordre inverse doit également provoquer un échec ; sinon, le résultat dépendrait de l’ordre des entrées dans l’archive.

Les différences de casse et les noms équivalents en Unicode exigent une politique supplémentaire. Si les données importées n’ont pas besoin de distinguer ces noms, vous pouvez appliquer aux composants une normalisation Unicode sous forme composée et un repli de casse pour détecter les conflits, tout en conservant les noms d’origine lors de l’écriture. Ne déduisez pas le comportement d’écriture sur iOS de la sensibilité à la casse du volume utilisé sur le Mac dans le cloud. Préparez au minimum deux groupes de cas de test, l’un pour les collisions de casse et l’autre pour les caractères combinés, puis vérifiez-les à nouveau dans le simulateur cible.

Vérifier les quotas lors de l’écriture réelle

La couche d’adaptation doit recevoir les blocs de données décompressées avant leur écriture, vérifier la taille cumulée, puis seulement écrire dans le fichier. Comparez la taille du bloc à la capacité restante au lieu d’additionner directement les tailles, afin d’éviter un débordement d’entier. Si la bibliothèque ne peut décompresser qu’en une seule opération en mémoire, sans permettre de limiter la sortie en cours de traitement, changez d’interface ou imposez des contraintes strictes supplémentaires aux données d’entrée.

Définissez un résultat attendu pour chaque archive de test : une arborescence normale est acceptée, un chemin qui sort du périmètre est refusé, une destination en double est refusée et le dépassement de n’importe quel quota est refusé. Ajoutez aussi des données compressées corrompues pour vérifier qu’une erreur de lecture n’est pas interprétée comme une fin normale du flux. Même pour les archives valides, vérifiez le résultat du contrôle d’intégrité fourni par la bibliothèque, et pas seulement la présence des fichiers finaux.

Après un échec d’écriture, vérifiez par des assertions que le répertoire de staging a été supprimé, que le répertoire de l’application n’a pas changé et que le contenu d’un fichier sentinelle placé au préalable à côté du répertoire d’import est resté intact. Ces vérifications sont plus utiles qu’une simple assertion sur la levée d’une erreur : celle-ci peut survenir après qu’un fichier a déjà été écrit hors du périmètre autorisé.

Intégrer les contrôles à la validation de non-régression et préserver le périmètre

Dans le pipeline, exécutez d’abord le script de validation des chemins, puis les tests de l’application qui font appel au véritable extracteur. Consignez l’identifiant de l’échantillon, le motif du refus et le nombre d’octets réellement écrits, sans enregistrer le contenu des fichiers utilisateur. Créez des répertoires de test aux noms aléatoires et supprimez-les à la fin. N’utilisez jamais le répertoire temporaire du système lui-même comme racine d’extraction.

Un répertoire peut encore être remplacé entre le contrôle préalable et l’écriture. Utilisez un répertoire de staging privé à l’application, que les autres tâches ne peuvent pas modifier, et refusez de suivre les liens pendant l’écriture. Dans un script exécuté sur un environnement de bureau partagé où d’autres processus peuvent modifier les répertoires, un contrôle du préfixe du chemin sous forme de chaîne ne suffit pas à éliminer ce risque.

Vérifiez enfin trois points : un échec ne laisse aucune donnée d’import partielle, un succès ne produit que les fichiers attendus et les quotas sont appliqués au nombre d’octets réellement écrits. La fonction de validation des chemins constitue le premier filtre ; les assertions sur les écritures issues de véritables archives constituent le critère final de validation.

Questions fréquentes

Suffit-il de refuser les noms contenant deux points ?

Non. Refusez le composant de chemin .., les chemins absolus et les séparateurs non pris en charge. Vérifiez aussi les liens, les destinations en double et les limites du répertoire lors de l’écriture. report..txt n’est pas dangereux en soi.

La taille annoncée dans le ZIP suffit-elle à bloquer une bombe de décompression ?

Non. Elle sert au contrôle initial, mais il faut compter les octets réellement décompressés pendant l’écriture. Limitez chaque entrée, le total et le nombre d’entrées. En cas de dépassement, interrompez l’import et supprimez le répertoire temporaire.

Confiez vos builds à un Mac cloud dédié

Choisissez un nœud physique, un modèle et une durée de facturation pour exécuter vos builds Xcode et vos tâches d’automatisation dans votre propre environnement macOS. L’état de disponibilité est indiqué en temps réel dans la console.

Poursuivez la vérification de votre environnement de build

Comparez les caractéristiques des modèles et les instructions pour définir la configuration à suivre.