Gestire archivi ZIP in Python in modo pratico e sicuro

by theArchitect
SHARE
Gestire archivi ZIP in Python in modo pratico e sicuro
© Guida-HTML5.it

Introduzione

Gli archivi ZIP sono molto utilizzati per raggruppare più file in un unico contenitore: report, documenti, immagini, backup e pacchetti da trasferire. Python include il modulo standard zipfile, che permette di creare, leggere, aggiornare ed estrarre archivi ZIP senza installare librerie esterne.

In questo tutorial costruiremo un piccolo programma capace di:

  • creare un archivio ZIP a partire da una cartella;
  • visualizzare l’elenco dei file contenuti;
  • estrarre i file in una directory di destinazione;
  • controllare che l’estrazione non possa uscire dalla cartella prevista.

L’ultimo punto è particolarmente importante. Un archivio proveniente da Internet o da un utente non fidato potrebbe contenere percorsi come ../../config.ini. Estrarre direttamente questi percorsi può sovrascrivere file esterni alla directory scelta, causando una vulnerabilità nota come Zip Slip.

Codice completo

from pathlib import Path
from zipfile import ZipFile, ZIP_DEFLATED
import sys


def crea_archivio(cartella_origine: Path, archivio: Path) -> None:
    """Crea un archivio ZIP contenente tutti i file della cartella."""
    if not cartella_origine.is_dir():
        raise NotADirectoryError(
            f"La cartella non esiste: {cartella_origine}"
        )

    # "w" crea un nuovo archivio oppure sovrascrive quello esistente.
    with ZipFile(archivio, mode="w", compression=ZIP_DEFLATED) as zip_file:
        for percorso in cartella_origine.rglob("*"):
            if percorso.is_file():
                # arcname evita di salvare nel file ZIP il percorso assoluto.
                nome_nell_archivio = percorso.relative_to(cartella_origine)
                zip_file.write(percorso, arcname=nome_nell_archivio)


def elenca_contenuto(archivio: Path) -> None:
    """Stampa nomi e dimensioni dei file presenti nell´archivio."""
    with ZipFile(archivio, mode="r") as zip_file:
        print(f"Contenuto di {archivio}:")
        for elemento in zip_file.infolist():
            dimensione = elemento.file_size
            print(f"- {elemento.filename} ({dimensione} byte)")


def estrai_sicuro(archivio: Path, destinazione: Path) -> None:
    """Estrae l´archivio impedendo percorsi fuori dalla destinazione."""
    destinazione = destinazione.resolve()
    destinazione.mkdir(parents=True, exist_ok=True)

    with ZipFile(archivio, mode="r") as zip_file:
        for elemento in zip_file.infolist():
            percorso_finale = (destinazione / elemento.filename).resolve()

            # Verifica che il percorso finale rimanga dentro la destinazione.
            try:
                percorso_finale.relative_to(destinazione)
            except ValueError as errore:
                raise ValueError(
                    f"Percorso non sicuro nell´archivio: {elemento.filename}"
                ) from errore

        # Se tutti i percorsi sono validi, si può procedere all´estrazione.
        zip_file.extractall(destinazione)


def main() -> None:
    if len(sys.argv) != 2:
        print("Uso: python archivi_zip.py <comando>")
        print("Comandi: crea, lista, estrai")
        return

    comando = sys.argv[1]
    cartella = Path("documenti")
    archivio = Path("documenti.zip")
    destinazione = Path("estratti")

    if comando == "crea":
        crea_archivio(cartella, archivio)
        print(f"Archivio creato: {archivio}")

    elif comando == "lista":
        elenca_contenuto(archivio)

    elif comando == "estrai":
        estrai_sicuro(archivio, destinazione)
        print(f"File estratti in: {destinazione.resolve()}")

    else:
        print(f"Comando non riconosciuto: {comando}")


if __name__ == "__main__":
    main()

Spiegazione

Creare un archivio

La classe ZipFile può essere utilizzata con un blocco with. In questo modo l’archivio viene chiuso automaticamente anche se durante l’operazione si verifica un errore.

La modalità w indica la scrittura di un nuovo archivio. L’opzione ZIP_DEFLATED abilita la compressione standard basata su zlib. Il metodo rglob("*") cerca ricorsivamente tutti gli elementi presenti nella cartella e nelle sue sottocartelle.

Il parametro arcname è fondamentale: salva nell’archivio un percorso relativo, ad esempio fatture/gennaio.pdf, invece di un percorso completo come /home/mario/documenti/fatture/gennaio.pdf. Non è una buona pratica inserire nell’archivio informazioni locali sul computer che lo ha creato.

Leggere l’elenco dei contenuti

Il metodo infolist() restituisce una sequenza di oggetti ZipInfo. Ogni oggetto contiene informazioni come il nome del file, la dimensione originale e la data di modifica. Questa funzionalità è utile per mostrare un’anteprima senza estrarre nulla.

Estrarre i file in sicurezza

Il metodo extractall() è comodo, ma non dovrebbe essere chiamato senza verificare i nomi contenuti nell’archivio. Per ogni elemento, il programma combina la directory di destinazione con il nome del file e risolve il percorso tramite resolve().

Successivamente, relative_to() controlla che il percorso risultante sia effettivamente contenuto nella directory prevista. Se un nome tenta di risalire verso directory superiori, viene sollevata un’eccezione e l’estrazione viene bloccata.

Eseguire il programma

Creiamo una cartella chiamata documenti e inseriamo alcuni file al suo interno. Poi possiamo usare questi comandi:

python archivi_zip.py crea
python archivi_zip.py lista
python archivi_zip.py estrai

Il primo comando produce documenti.zip, il secondo ne visualizza il contenuto e il terzo estrae i file nella cartella estratti.

Best practice

  • Usa sempre il contesto with: garantisce la chiusura corretta dell’archivio.
  • Controlla i percorsi prima dell’estrazione: non fidarti dei nomi memorizzati in archivi ricevuti da fonti esterne.
  • Evita i percorsi assoluti: nell’archivio usa percorsi relativi tramite relative_to().
  • Verifica le dimensioni: un archivio molto piccolo potrebbe espandersi enormemente durante l’estrazione, causando un attacco di tipo “zip bomb”.
  • Gestisci gli errori: puoi catturare BadZipFile quando il file non è un archivio ZIP valido.
  • Non sovrascrivere dati importanti senza conferma: estrarre un archivio può sostituire file già presenti nella destinazione.
  • Usa nomi coerenti: mantieni estensioni e strutture di cartelle comprensibili per facilitare manutenzione e diagnosi.

Per gestire archivi provenienti da utenti, è inoltre consigliabile applicare limiti sulla quantità totale di file, sulla dimensione compressa e sulla dimensione prevista dopo l’estrazione.

Riepilogo

Il modulo zipfile offre tutto ciò che serve per lavorare con archivi ZIP in Python. Con ZipFile puoi creare e leggere archivi, mentre write(), infolist() ed extractall() coprono le operazioni principali.

La parte più importante non è soltanto comprimere i file, ma farlo in modo prevedibile e sicuro. Usare percorsi relativi, controllare i nomi prima dell’estrazione e limitare le risorse impiegate rende il programma più robusto anche quando lavora con dati esterni.

Approfondisci con risorse ufficiali

SHARE