Crie um arquivo com segurança se e somente se ele não existir com o python

93

Desejo gravar em um arquivo com base no fato de esse arquivo já existir ou não, somente gravando se ele ainda não existir (na prática, desejo continuar tentando arquivos até encontrar um que não exista).

O código a seguir mostra uma maneira pela qual um invasor em potencial pode inserir um link simbólico, conforme sugerido nesta postagem , entre um teste para o arquivo e o arquivo que está sendo escrito. Se o código for executado com permissões altas o suficiente, isso pode substituir um arquivo arbitrário.

Existe alguma maneira de resolver 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
fonte
Verifique a escrita atômica com Python stackoverflow.com/questions/2333872/…
Mikko Ohtamaa
@Mikko Isso não ajuda aqui.
Konrad Rudolph
Ah ok. Eu entendi qual é o problema ... você escreve SÓ se o arquivo existir?
Mikko Ohtamaa
Você poderia gravar o arquivo em um local temporário e, em seguida, executar um comando de cópia sem permitir a substituição?
Eric

Respostas:

93

Editar : Veja também a resposta de Dave Jones : no Python 3.3, você pode usar o xsinalizador open()para fornecer essa função.

Resposta original abaixo

Sim, mas não usando a open()chamada padrão do Python . Em os.open()vez disso, você precisará usar , o que permite especificar sinalizadores para o código C subjacente.

Em particular, você deseja usar O_CREAT | O_EXCL. A partir da página homem por open(2)debaixo O_EXCLno meu sistema Unix:

Certifique-se de que esta chamada cria o arquivo: se este sinalizador for especificado em conjunto com O_CREAT, e o nome do caminho já existir, open()haverá falha. O comportamento de O_EXCLé indefinido se O_CREATnão for especificado.

Quando esses dois sinalizadores são especificados, os links simbólicos não são seguidos: se o nome do caminho for um link simbólico, ele open()falhará independentemente de para onde o link simbólico aponta.

O_EXCL só é compatível com NFS ao usar NFSv3 ou posterior no kernel 2.6 ou posterior. Em ambientes onde o O_EXCLsuporte a NFS não é fornecido, os programas que dependem dele para executar tarefas de bloqueio conterão uma condição de corrida.

Portanto, não é perfeito, mas AFAIK é o mais perto que você pode chegar de evitar essa condição de corrida.

Editar: as outras regras de uso em os.open()vez de open()ainda se aplicam. Em particular, se você quiser usar o descritor de arquivo retornado para ler ou escrever, você vai precisar de um dos O_RDONLY, O_WRONLYou O_RDWRbandeiras também.

Todos os O_*sinalizadores estão no osmódulo do Python , então você precisará import osusar os.O_CREATetc.

Exemplo:

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!")
eu e
fonte
1
1 para a resposta obviamente correta. Pessoalmente, estou curioso para saber quantas pessoas realmente têm problemas com a ressalva do NFS - eu (talvez de forma imprudente) a rejeito como um ambiente obsoleto em que meu código nunca deveria ser executado.
zigg
2
@zigg: NFSv3 é de 1995, então parece justo considerar as versões mais antigas como obsoletas.
Fred Foo
1
Eu ficaria mais preocupado com a versão do kernel, pessoalmente. Se você estiver executando qualquer coisa que se pareça vagamente com um sistema atualizado, você não deve ter problemas, mas o RHEL 3 (ainda em fase de suporte estendido) está executando um kernel 2.4, por exemplo. Além disso, não investiguei se eles fornecem gravações atômicas no Windows em FAT ou NTFS, o que é uma limitação potencialmente importante.
me_e
1
@me_and A página python nas constantes de sinalização aberta sugere que isso funciona bem com o Windows. Vou tentar em breve!
Henry Gomersall
1
Verdade, mas eu não vi em nenhum lugar (incluindo MSDN ) que diga explicitamente que essas sinalizações fornecem a criação de arquivos atômicos . Possivelmente estou sendo muito paranóico, mas gostaria de ver a palavra-chave "atômica" antes de confiar nela para qualquer coisa que seja crítica para a segurança.
me_e
69

Para referência, Python 3.3 implementa um novo 'x'modo na open()função para cobrir este caso de uso (criar apenas, falhar se o arquivo existir). Observe que o 'x'modo é especificado por conta própria. Usar 'wx'resultados em a ValueErrorcomo o 'w'é redundante (a única coisa que você pode fazer se a chamada for bem-sucedida é gravar no arquivo; ele não pode ter existido se a chamada for bem-sucedida):

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

Para Python 3.2 e inferior (incluindo Python 2.x), consulte a resposta aceita .

Dave Jones
fonte
Boa sugestão. Infelizmente, isso parece ser apenas POSIX (não funciona no 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
Você está usando o python 3.2; o modo 'x' está em 3.3 e superior, mas é plataforma cruzada. A propósito, você só usa 'x' em vez de 'wx' - o modo de gravação é redundante, pois a única coisa que você pode fazer com o arquivo é gravá-lo de qualquer maneira
Dave Jones
Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Szabolcs Dombi
1
Vou servir - embora tenha que esperar até que eu esteja de volta ao computador um pouco mais tarde.
Dave Jones
2
É razoável abrir um arquivo existente para gravação, mas o objetivo do modo 'x' é abrir o arquivo se e somente se ele ainda não existir , falhando com um erro quando o arquivo existir. É por isso que é redundante com o sinalizador 'w'; se for bem-sucedido, o arquivo estará vazio (e, portanto, não adianta ler dele :).
Dave Jones
0

Este código criará facilmente um ARQUIVO se não houver um.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 
user2033758
fonte
15
Sim vai. O ponto importante da pergunta era o aspecto da segurança. O problema é que entre identificar a presença do arquivo e usá-lo ou criá-lo, algo pode mudar e resultar em um resultado ruim (como na pergunta original).
Henry Gomersall
5
Isso é verdade. Chama-se TOCTOU!
Rad
Se outro processo criar e gravar no arquivo após a ifinstrução, esse código apagará o arquivo.
Peter Wood,