Cree un archivo de forma segura si y solo si no existe con Python

93

Deseo escribir en un archivo en función de si ese archivo ya existe o no, solo escribiendo si aún no existe (en la práctica, deseo seguir probando archivos hasta encontrar uno que no existe).

El siguiente código muestra una forma en que un atacante potencial podría insertar un enlace simbólico, como se sugiere en esta publicación, entre una prueba para el archivo y el archivo que se está escribiendo. Si el código se ejecuta con permisos suficientemente altos, esto podría sobrescribir un archivo arbitrario.

¿Hay alguna forma de solucionar este 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')
Henry Gomersall
fuente
Verifique la escritura atómica con Python stackoverflow.com/questions/2333872/…
Mikko Ohtamaa
@Mikko Eso no ayuda aquí.
Konrad Rudolph
Ah ok. Entendí cuál es el problema ... escribe SOLO si el archivo existe?
Mikko Ohtamaa
¿Podría escribir el archivo en una ubicación temporal y luego ejecutar un comando de copia sin permitir la sobrescritura?
Eric

Respuestas:

94

Editar : Vea también la respuesta de Dave Jones : desde Python 3.3, puede usar la xbandera open()para proporcionar esta función.

Respuesta original a continuación

Sí, pero sin usar la open()llamada estándar de Python . Deberá usar os.open()en su lugar, lo que le permite especificar indicadores en el código C subyacente.

En particular, desea utilizar O_CREAT | O_EXCL. Desde la página de manual de open(2)under O_EXCLen mi sistema Unix:

Asegúrese de que esta llamada cree el archivo: si este indicador se especifica junto con O_CREAT, y el nombre de ruta ya existe, open()fallará. El comportamiento de O_EXCLno está definido si O_CREATno se especifica.

Cuando se especifican estos dos indicadores, los enlaces simbólicos no se siguen: si el nombre de la ruta es un enlace simbólico, open()falla independientemente de dónde apunte el enlace simbólico.

O_EXCL solo es compatible con NFS cuando se usa NFSv3 o posterior en el kernel 2.6 o posterior. En entornos donde O_EXCLno se proporciona compatibilidad con NFS , los programas que dependen de él para realizar tareas de bloqueo contendrán una condición de carrera.

Así que no es perfecto, pero AFAIK es lo más cerca que puedes estar de evitar esta condición de carrera.

Editar: las otras reglas de uso en os.open()lugar de open()todavía se aplican. En particular, si desea utilizar el descriptor de archivo devuelto para leer o escribir, también necesitará uno de los indicadores O_RDONLY, O_WRONLYo O_RDWR.

Todas las O_*banderas están en el osmódulo de Python , por lo que necesitará import osusar, os.O_CREATetc.

Ejemplo:

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!")
yo y
fuente
1
+1 para la respuesta obviamente correcta. Personalmente, tengo curiosidad por saber cuántas personas tienen problemas con la advertencia de NFS: yo (tal vez imprudentemente) lo descarto como un entorno obsoleto en el que mi código nunca debería ejecutarse.
zigg
2
@zigg: NFSv3 es de 1995, por lo que parece justo considerar obsoletas las versiones anteriores.
Fred Foo
1
Personalmente, estaría más preocupado por la versión del kernel. Si está ejecutando algo que se parezca vagamente a un sistema actualizado, no debería tener ningún problema, pero RHEL 3 (todavía en fase de soporte extendido) está ejecutando un kernel 2.4, por ejemplo. Además, no he investigado si proporcionan escrituras atómicas en Windows en FAT o NTFS, que es una limitación potencialmente importante.
yo_y
1
@me_and La página de Python sobre constantes de bandera abierta sugiere que esto funciona bien con Windows. ¡Lo intentaré en breve!
Henry Gomersall
1
Es cierto, pero no he visto en ningún lugar (incluido MSDN ) que diga explícitamente que estos indicadores dan creación de archivos atómicos . Posiblemente estoy siendo demasiado paranoico, pero me gustaría ver esa palabra clave "atómica" antes de confiar en esto para cualquier cosa que sea crítica para la seguridad.
yo y el
70

Como referencia, Python 3.3 implementa un nuevo 'x'modo en la open()función para cubrir este caso de uso (solo crear, fallar si el archivo existe). Tenga en cuenta que el 'x'modo se especifica por sí solo. El uso de 'wx'resultados en a ValueErrorcomo 'w'es redundante (lo único que puede hacer si la llamada tiene éxito es escribir en el archivo de todos modos; no puede haber existido si la llamada tiene éxito):

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

Para Python 3.2 y versiones anteriores (incluido Python 2.x), consulte la respuesta aceptada .

Dave Jones
fuente
Buena sugerencia. Desafortunadamente, esto parece ser solo POSIX (no funciona en 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
Estás usando Python 3.2; el modo 'x' está en 3.3 y superior pero es multiplataforma. Por cierto, solo usa 'x' en lugar de 'wx'; el modo de escritura es redundante, ya que lo único que puede hacer con el archivo es escribir en él de todos modos
Dave Jones
Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Szabolcs Dombi
1
Lo haré, aunque tendré que esperar hasta que vuelva a estar frente a una computadora un poco más tarde.
Dave Jones
2
Es razonable abrir un archivo existente para escritura, pero el objetivo del modo 'x' es abrir el archivo si y solo si aún no existe , fallando con un error cuando el archivo sí existe. Por eso es redundante con la bandera 'w'; si tiene éxito, se garantiza que el archivo estará vacío (y, por lo tanto, tiene muy poco sentido leerlo :).
Dave Jones
0

Este código creará fácilmente un ARCHIVO si no existe uno.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 
usuario2033758
fuente
16
Sí lo será. El punto importante de la pregunta fue el aspecto de seguridad. El problema es que entre identificar la presencia del archivo y usarlo o crearlo, algo podría cambiar y dar como resultado un mal resultado (como en la pregunta original).
Henry Gomersall
5
Es verdad. ¡Se llama TOCTOU!
Rad
Si otro proceso crea y escribe en el archivo después de la ifdeclaración, este código borrará el archivo.
Peter Wood