Безопасно создайте файл тогда и только тогда, когда он не существует с python

93

Я хочу писать в файл в зависимости от того, существует ли этот файл уже или нет, записывая только в том случае, если он еще не существует (на практике я хочу продолжать пробовать файлы, пока не найду тот, которого не существует).

В следующем коде показано, как потенциально злоумышленник может вставить символическую ссылку, как это предлагается в этом сообщении, между тестом файла и записываемым файлом. Если код запускается с достаточно высокими разрешениями, это может привести к перезаписи произвольного файла.

Есть ли способ решить эту проблему?

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')
Генри Гомерсолл
источник
Проверьте атомарную запись с помощью Python stackoverflow.com/questions/2333872/…
Микко Охтамаа
@Mikko Это здесь не помогает.
Конрад Рудольф
Ах хорошо. Я понял в чем дело ... вы пишете ТОЛЬКО если файл существует?
Mikko Ohtamaa
Не могли бы вы записать файл во временное место, а затем выполнить команду копирования, не разрешая перезапись?
Эрик

Ответы:

94

Изменить : см. Также ответ Дэйва Джонса : из Python 3.3 вы можете использовать xфлаг open()для предоставления этой функции.

Оригинальный ответ ниже

Да, но без использования стандартного open()вызова Python . os.open()Вместо этого вам нужно будет использовать , что позволяет вам указывать флаги для базового кода C.

В частности, вы хотите использовать O_CREAT | O_EXCL. На странице open(2)руководства O_EXCLдля моей системы Unix:

Убедитесь, что этот вызов создает файл: если этот флаг указан вместе с O_CREAT, и имя пути уже существует, open()произойдет сбой. Поведение O_EXCLне определено, если O_CREATне указано.

Когда указаны эти два флага, по символическим ссылкам не следуют: если путь является символической ссылкой, происходит open()сбой независимо от того, на что указывает символическая ссылка.

O_EXCL поддерживается только в NFS при использовании NFSv3 или новее в ядре 2.6 или новее. В средах, где O_EXCLподдержка NFS не предусмотрена, программы, которые используют ее для выполнения задач блокировки, будут содержать состояние гонки.

Так что это не идеально, но, как ни странно, это самое близкое к тому, чтобы избежать этого состояния гонки.

Изменить: другие правила использования os.open()вместо по- open()прежнему применяются. В частности, если вы хотите использовать возвращенный файловый дескриптор для чтения или записи, вам также понадобится один из флагов O_RDONLY, O_WRONLYили O_RDWR.

Все O_*флаги находятся в osмодуле Python , поэтому вам нужно будет import osиспользовать os.O_CREATи т. Д.

Пример:

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 за заведомо правильный ответ. Мне лично любопытно узнать, у скольких людей на самом деле есть проблемы с предупреждением о NFS - я (возможно, опрометчиво) отклоняю его как устаревшую среду, в которой никогда не следует запускать мой код.
zigg
2
@zigg: NFSv3 выпущен в 1995 году, поэтому кажется справедливым считать старые версии устаревшими.
Fred Foo
1
Лично меня больше беспокоит версия ядра. Если вы используете что-то, хотя бы отдаленно напоминающее современную систему, у вас не должно возникнуть проблем, но, например, RHEL 3 (все еще находящийся на этапе расширенной поддержки) работает под управлением ядра 2.4. Кроме того, я не исследовал, обеспечивают ли они атомарную запись в Windows в FAT или NTFS, что является потенциально серьезным ограничением.
me_and
1
@me_and Страница Python с константами открытых флагов предполагает, что это нормально работает с Windows. Я скоро попробую!
Генри Гомерсалл
1
Верно, но я не встречал нигде (включая MSDN ), где бы явно говорилось, что эти флаги позволяют создавать атомарные файлы. Возможно, я чрезмерно параноик, но я бы хотел увидеть это «атомарное» ключевое слово, прежде чем доверять ему что-либо, что критично с точки зрения безопасности.
me_and
71

Для справки, Python 3.3 реализует новый 'x'режим в open()функции для покрытия этого варианта использования (только создание, сбой, если файл существует). Обратите внимание, что 'x'режим указывается отдельно. Использование 'wx'результатов в ValueErrorкачестве 'w'избыточна (единственное , что вы можете сделать , если вызов удачен является запись в файл в любом случае, она не может существовать , если вызов успешен):

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

Для Python 3.2 и ниже (включая Python 2.x) см. Принятый ответ .

Дэйв Джонс
источник
Хорошее предложение. К сожалению, похоже, что это только POSIX (не работает в 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'
Дэн Ленски
5
Вы используете python 3.2; режим «x» есть в версии 3.3 и выше, но он кроссплатформенный. Между прочим, вы используете только 'x' вместо 'wx' - режим записи избыточен, поскольку единственное, что вы могли бы сделать с файлом, это все равно записать в него
Дэйв Джонс
Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Сабольч Домби
1
Подойдет - хотя придется подождать, пока я вернусь к компьютеру чуть позже.
Дэйв Джонс
2
Разумно открывать существующий файл для записи, но вся суть режима 'x' заключается в том, чтобы открыть файл тогда и только тогда, когда он еще не существует , с ошибкой, когда файл действительно существует. Вот почему он избыточен с флагом «w»; в случае успеха файл гарантированно будет пустым (и, следовательно, из него будет считываться очень мало смысла :).
Дэйв Джонс
0

Этот код легко создаст ФАЙЛ, если он не существует.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 
user2033758
источник
16
Да, это будет. Важным моментом в вопросе был аспект безопасности. Проблема в том, что между определением наличия файла и его использованием или созданием что-то может измениться, что приведет к плохому результату (как в исходном вопросе).
Генри Гомерсалл
5
Это правда. Это называется ТОКТУ!
Rad
Если другой процесс создает и записывает в файл после ifоператора, этот код очистит файл.
Питер Вуд