Créez un fichier en toute sécurité si et seulement s'il n'existe pas avec python

93

Je souhaite écrire dans un fichier en fonction du fait que ce fichier existe déjà ou non, en n'écrivant que s'il n'existe pas déjà (en pratique, je souhaite continuer à essayer des fichiers jusqu'à ce que j'en trouve un qui n'existe pas).

Le code suivant montre une manière dont un attaquant potentiel pourrait insérer un lien symbolique, comme suggéré dans cet article entre un test pour le fichier et le fichier en cours d'écriture. Si le code est exécuté avec des autorisations suffisamment élevées, cela pourrait écraser un fichier arbitraire.

Existe-t-il un moyen de résoudre ce problème?

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')
Henry Gomersall
la source
Vérifiez l'écriture atomique avec Python stackoverflow.com/questions/2333872/…
Mikko Ohtamaa
@Mikko Cela n'aide pas ici.
Konrad Rudolph
Ah ok. J'ai compris quel est le problème ... vous écrivez UNIQUEMENT si le fichier existe?
Mikko Ohtamaa du
Pourriez-vous écrire le fichier dans un emplacement temporaire, puis exécuter une commande de copie sans autoriser l'écrasement?
Eric

Réponses:

93

Edit : Voir aussi la réponse de Dave Jones : à partir de Python 3.3, vous pouvez utiliser l' xindicateur open()pour fournir cette fonction.

Réponse originale ci-dessous

Oui, mais sans utiliser l' open()appel standard de Python . Vous devrez utiliser à la os.open()place, ce qui vous permet de spécifier des indicateurs pour le code C sous-jacent.

En particulier, vous souhaitez utiliser O_CREAT | O_EXCL. Depuis la page de manuel de open(2)under O_EXCLsur mon système Unix:

Assurez-vous que cet appel crée le fichier: si cet indicateur est spécifié en conjonction avec O_CREAT, et que le chemin existe déjà, alors open()échouera. Le comportement de O_EXCLn'est pas défini s'il O_CREATn'est pas spécifié.

Lorsque ces deux indicateurs sont spécifiés, les liens symboliques ne sont pas suivis: si le chemin d'accès est un lien symbolique, alors open()échoue quel que soit l'endroit où le lien symbolique pointe.

O_EXCL est uniquement pris en charge sur NFS lors de l'utilisation de NFSv3 ou version ultérieure sur le noyau 2.6 ou version ultérieure. Dans les environnements où la O_EXCLprise en charge NFS n'est pas fournie, les programmes qui en dépendent pour effectuer des tâches de verrouillage contiendront une condition de concurrence.

Ce n'est donc pas parfait, mais AFAIK est le plus proche pour éviter cette condition de course.

Edit: les autres règles d'utilisation os.open()au lieu de open()s'appliquent toujours. En particulier, si vous souhaitez utiliser le descripteur de fichier renvoyé pour la lecture ou l'écriture, vous aurez également besoin de l'un des indicateurs O_RDONLY, O_WRONLYou O_RDWR.

Tous les O_*indicateurs sont dans le osmodule de Python , vous devrez donc import oset utiliser os.O_CREATetc.

Exemple:

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!")
moi et
la source
1
+1 pour la réponse évidemment correcte. Je suis personnellement curieux de savoir combien de personnes ont réellement des problèmes avec la mise en garde NFS - je la rejette (peut-être par imprudence) comme un environnement obsolète sur lequel mon code ne devrait jamais être exécuté.
zigg
2
@zigg: NFSv3 date de 1995, il semble donc juste de considérer les anciennes versions comme obsolètes.
Fred Foo
1
Je serais plus inquiet pour la version du noyau, personnellement. Si vous exécutez quelque chose qui ressemble même vaguement à un système à jour, vous ne devriez avoir aucun problème, mais RHEL 3 (toujours en phase de support étendu) exécute un noyau 2.4, par exemple. De plus, je n'ai pas cherché à savoir s'ils fournissent des écritures atomiques sur Windows sur FAT ou NTFS, ce qui est une limitation potentiellement majeure.
me_and
1
@me_and La page python sur les constantes d'indicateur ouvert suggère que cela fonctionne bien avec Windows. Je vais l'essayer sous peu!
Henry Gomersall
1
C'est vrai, mais je n'ai vu nulle part (y compris MSDN ) qui dit explicitement que ces indicateurs donnent la création de fichiers atomiques . Je suis peut-être trop paranoïaque, mais je voudrais voir ce mot-clé "atomique" avant de lui faire confiance pour tout ce qui est critique pour la sécurité.
me_and
69

Pour référence, Python 3.3 implémente un nouveau 'x'mode dans la open()fonction pour couvrir ce cas d'utilisation (créer uniquement, échouer si le fichier existe). Notez que le 'x'mode est spécifié seul. L'utilisation des 'wx'résultats dans a ValueErroras the 'w'est redondante (la seule chose que vous pouvez faire si l'appel réussit est de toute façon d'écrire dans le fichier; cela ne peut pas avoir existé si l'appel réussit):

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

Pour Python 3.2 et inférieur (y compris Python 2.x), veuillez vous référer à la réponse acceptée .

Dave Jones
la source
Bonne suggestion. Malheureusement, cela semble être uniquement POSIX (ne fonctionne pas sous 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
Vous utilisez python 3.2; le mode «x» est en 3.3 et au-dessus, mais il est multiplateforme. Incidemment, vous n'utilisez que 'x' au lieu de 'wx' - le mode d'écriture est redondant car la seule chose que vous pouvez faire avec le fichier est de toute façon d'y écrire
Dave Jones
Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Szabolcs Dombi
1
Va faire - mais il faudra attendre un peu plus tard que je sois de retour devant un ordinateur.
Dave Jones
2
Il est raisonnable d'ouvrir un fichier existant pour l'écriture, mais le point entier du mode 'x' est d'ouvrir le fichier si et seulement s'il n'existe pas déjà , en échouant avec une erreur lorsque le fichier existe. C'est pourquoi il est redondant avec le drapeau «w»; s'il réussit, le fichier est garanti vide (et par conséquent, il est très peu utile de le lire :).
Dave Jones
0

Ce code créera facilement un FICHIER s'il n'en existe pas.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 
user2033758
la source
15
Oui, il sera. Le point important de la question était l'aspect sécurité. Le problème est qu'entre l'identification de la présence du fichier et son utilisation ou sa création, quelque chose peut changer et entraîner un mauvais résultat (comme dans la question d'origine).
Henry Gomersall
5
C'est vrai. Ça s'appelle TOCTOU!
Rad
Si un autre processus crée et écrit dans le fichier après l' ifinstruction, ce code efface le fichier.
Peter Wood