Crea un file in modo sicuro se e solo se non esiste con python


93

Desidero scrivere su un file in base al fatto che quel file esista già o meno, scrivendo solo se non esiste già (in pratica, desidero continuare a provare i file finché non ne trovo uno che non esiste).

Il codice seguente mostra un modo in cui un potenziale utente malintenzionato potrebbe inserire un collegamento simbolico, come suggerito in questo post tra un test per il file e il file in fase di scrittura. Se il codice viene eseguito con autorizzazioni sufficientemente elevate, ciò potrebbe sovrascrivere un file arbitrario.

C'è un modo per risolvere questo problema?

import os
import errno

file_to_be_attacked = 'important_file'

with open(file_to_be_attacked, 'w') as f:
    f.write('Some important content!\n')

test_file = 'testfile'

try:
    with open(test_file) as f: pass
except IOError, e:

    # symlink created here
    os.symlink(file_to_be_attacked, test_file)

    if e.errno != errno.ENOENT:
        raise
    else:
        with open(test_file, 'w') as f:
            f.write('Hello, kthxbye!\n')

Controllare la scrittura atomica con Python stackoverflow.com/questions/2333872/...
Mikko Ohtamaa

@Mikko Questo non aiuta qui.
Konrad Rudolph,

Ah ok. Ho capito qual è il problema ... scrivi SOLO se il file esiste?
Mikko Ohtamaa,

Potresti scrivere il file in una posizione temporanea, quindi eseguire un comando di copia senza consentire la sovrascrittura?
Eric,

Risposte:


93

Modifica : vedi anche la risposta di Dave Jones : da Python 3.3, puoi usare il xflag open()per fornire questa funzione.

Risposta originale di seguito

Sì, ma non usando la open()chiamata standard di Python . Dovrai os.open()invece usare , che ti consente di specificare i flag per il codice C sottostante.

In particolare, vuoi usare O_CREAT | O_EXCL. Dalla pagina man di open(2)sotto O_EXCLsul mio sistema Unix:

Assicurati che questa chiamata crei il file: se questo flag è specificato insieme a O_CREAT, e il percorso esiste già, allora open()fallirà. Il comportamento di O_EXCLnon è definito se O_CREATnon è specificato.

Quando vengono specificati questi due flag, i collegamenti simbolici non vengono seguiti: se il percorso è un collegamento simbolico, open()fallisce indipendentemente da dove punta il collegamento simbolico.

O_EXCL è supportato solo su NFS quando si utilizza NFSv3 o successivo sul kernel 2.6 o successivo. Negli ambienti in cui O_EXCLnon viene fornito il supporto NFS , i programmi che si basano su di esso per l'esecuzione di attività di blocco conterranno una condizione di competizione.

Quindi non è perfetto, ma AFAIK è il massimo che puoi ottenere per evitare questa condizione di gara.

Modifica: le altre regole di utilizzo os.open()invece di sono open()ancora valide. In particolare, se vuoi usare il descrittore di file restituito per leggere o scrivere, avrai bisogno anche di uno dei flag O_RDONLY, O_WRONLYo O_RDWR.

Tutti i O_*flag sono nel osmodulo di Python , quindi dovrai import osusarli e così via os.O_CREAT.

Esempio:

import os
import errno

flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY

try:
    file_handle = os.open('filename', flags)
except OSError as e:
    if e.errno == errno.EEXIST:  # Failed as the file already exists.
        pass
    else:  # Something unexpected went wrong so reraise the exception.
        raise
else:  # No exception, so the file must have been created successfully.
    with os.fdopen(file_handle, 'w') as file_obj:
        # Using `os.fdopen` converts the handle to an object that acts like a
        # regular Python file object, and the `with` context manager means the
        # file will be automatically closed when we're done with it.
        file_obj.write("Look, ma, I'm writing to a new file!")

1
+1 per la risposta ovviamente corretta. Personalmente sono curioso di sapere quante persone hanno effettivamente problemi con l'avvertenza di NFS: io (forse incautamente) lo scarto come un ambiente obsoleto su cui il mio codice non dovrebbe mai essere eseguito.
zigg

2
@zigg: NFSv3 è del 1995, quindi sembra giusto considerare obsolete le versioni precedenti.
Fred Foo,

1
Personalmente sarei più preoccupato per la versione del kernel. Se stai eseguendo qualcosa che assomigli anche vagamente a un sistema aggiornato, non dovresti avere problemi, ma RHEL 3 (ancora in fase di supporto esteso) esegue un kernel 2.4, per esempio. Inoltre, non ho verificato se forniscono scritture atomiche su Windows su FAT o NTFS, che è una limitazione potenzialmente importante.
io_e l'

1
@me_and La pagina python sulle costanti flag aperte suggerisce che funziona bene con Windows. Lo proverò a breve!
Henry Gomersall,

1
Vero, ma non ho visto da nessuna parte (incluso MSDN ) che dica esplicitamente che questi flag danno la creazione di file atomici . Forse sono eccessivamente paranoico, ma vorrei vedere quella parola chiave "atomica" prima di fidarmi di questa per qualsiasi cosa che sia critica per la sicurezza.
io_e l'

69

Per riferimento, Python 3.3 implementa una nuova 'x'modalità nella open()funzione per coprire questo caso d'uso (crea solo, fallisce se il file esiste). Notare che la 'x'modalità è specificata da sola. L'utilizzo dei 'wx'risultati in a ValueErrorcome 'w'è ridondante (l'unica cosa che puoi fare se la chiamata ha successo è scrivere comunque sul file; non può essere esistito se la chiamata ha successo):

>>> f1 = open('new_binary_file', 'xb')
>>> f2 = open('new_text_file', 'x')

Per Python 3.2 e versioni precedenti (incluso Python 2.x) fare riferimento alla risposta accettata .


Buon suggerimento. Sfortunatamente questo sembra essere solo POSIX (non funziona su Windows):Python 3.2 (r32:88445, Feb 20 2011, 21:30:00) [MSC v.1500 64 bit (AMD64)] on win32 >>> open("c:/temp/foo.csv","wx") ValueError: invalid mode: 'wx'
Dan Lenski

5
Stai usando python 3.2; la modalità "x" è in 3.3 e versioni successive ma è multipiattaforma. Per inciso, usi solo 'x' invece di 'wx' - la modalità di scrittura è ridondante poiché l'unica cosa che potresti fare con il file è scriverci comunque
Dave Jones

Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Szabolcs Dombi

1
Va bene, anche se dovrò aspettare finché non sarò di nuovo davanti a un computer un po 'più tardi.
Dave Jones

2
È ragionevole aprire un file esistente per la scrittura, ma l'intero punto della modalità "x" è aprire il file se e solo se non esiste già , fallendo con un errore quando il file esiste. Questo è il motivo per cui è ridondante con il flag "w"; se ha successo il file è garantito per essere vuoto (e quindi non ha molto senso leggerlo :).
Dave Jones

0

Questo codice creerà facilmente un FILE se non ne esiste uno.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 

15
Si lo farà. Il punto importante della domanda era l'aspetto della sicurezza. Il problema è che tra l'identificazione della presenza del file e il suo utilizzo o la sua creazione, potrebbe cambiare qualcosa che si traduce in un cattivo risultato (come nella domanda originale).
Henry Gomersall

5
È vero. Si chiama TOCTOU!
Rad

Se un altro processo crea e scrive nel file dopo l' ifistruzione, questo codice cancellerà il file.
Peter Wood
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.