Bezpiecznie utwórz plik wtedy i tylko wtedy, gdy nie istnieje w Pythonie

93

Chcę pisać do pliku na podstawie tego, czy ten plik już istnieje, czy nie, pisząc tylko wtedy, gdy jeszcze nie istnieje (w praktyce chciałbym próbować dalej, aż znajdę taki, który nie istnieje).

Poniższy kod przedstawia sposób, w jaki potencjalnie atakujący może wstawić łącze symboliczne, zgodnie z sugestią w tym poście, pomiędzy testem pliku a zapisywanym plikiem. Jeśli kod jest uruchamiany z wystarczająco wysokimi uprawnieniami, może to spowodować nadpisanie dowolnego pliku.

Czy jest jakiś sposób na rozwiązanie tego problemu?

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
źródło
Sprawdź pisanie atomowe w Pythonie stackoverflow.com/questions/2333872/ ...
Mikko Ohtamaa
@Mikko To tu nie pomaga.
Konrad Rudolph
Ach ok. Zrozumiałem, o co chodzi ... piszesz TYLKO jeśli plik istnieje?
Mikko Ohtamaa
Czy możesz zapisać plik w tymczasowej lokalizacji, a następnie wykonać polecenie kopiowania bez zezwolenia na nadpisanie?
Eric

Odpowiedzi:

94

Edycja : Zobacz także odpowiedź Dave'a Jonesa : od Pythona 3.3 możesz użyć xflagi, open()aby zapewnić tę funkcję.

Oryginalna odpowiedź poniżej

Tak, ale nie używam standardowego open()wywołania Pythona . Zamiast tego będziesz musiał użyć os.open(), co pozwala określić flagi do bazowego kodu C.

W szczególności chcesz użyć O_CREAT | O_EXCL. Ze strony podręcznika dla open(2)under O_EXCLw moim systemie Unix:

Upewnij się, że to wywołanie tworzy plik: jeśli ta flaga jest określona w połączeniu z O_CREAT, a ścieżka dostępu już istnieje, open()zakończy się niepowodzeniem. Zachowanie O_EXCLjest niezdefiniowane, jeśli O_CREATnie zostało określone.

Gdy te dwie flagi są określone, dowiązania symboliczne nie są przestrzegane: jeśli nazwa ścieżki jest dowiązaniem symbolicznym, to open()kończy się niepowodzeniem niezależnie od tego, gdzie wskazuje dowiązanie symboliczne.

O_EXCL jest obsługiwany tylko na NFS, gdy używany jest NFSv3 lub nowszy na jądrze 2.6 lub nowszym. W środowiskach, w których O_EXCLnie jest zapewniona obsługa NFS , programy, które polegają na nim przy wykonywaniu zadań blokowania, będą zawierać sytuację wyścigu.

Więc to nie jest idealne, ale AFAIK to najbliższy sposób uniknięcia tego wyścigu.

Edycja: pozostałe zasady używania os.open()zamiast open()nadal obowiązują. W szczególności, jeśli chcesz użyć zwróconego deskryptora pliku do odczytu lub zapisu, będziesz potrzebować również jednej z flag O_RDONLY, O_WRONLYlub O_RDWR.

Wszystkie O_*flagi znajdują się w osmodule Pythona , więc musisz import osi używać os.O_CREATitp.

Przykład:

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!")
ja i
źródło
1
+1 za oczywiście poprawną odpowiedź. Jestem osobiście ciekawy, ilu ludzi ma problemy z zastrzeżeniem NFS - ja (być może lekkomyślnie) odrzucam to jako przestarzałe środowisko, w którym mój kod nigdy nie powinien być uruchamiany.
zigg
2
@zigg: NFSv3 pochodzi z 1995 roku, więc wydaje się uczciwe, aby uważać starsze wersje za przestarzałe.
Fred Foo
1
Osobiście bardziej martwiłbym się wersją jądra. Jeśli używasz czegoś, co choć trochę przypomina aktualny system, nie powinieneś mieć problemu, ale na przykład RHEL 3 (wciąż w rozszerzonej fazie wsparcia) działa na jądrze 2.4. Nie badałem również, czy zapewniają one niepodzielne zapisy w systemie Windows na FAT lub NTFS, co jest potencjalnie głównym ograniczeniem.
me_and
1
@me_and Strona Pythona dotycząca stałych flag otwarcia sugeruje, że działa to dobrze w systemie Windows. Wkrótce spróbuję!
Henry Gomersall
1
To prawda, ale nie widziałem nigdzie (w tym MSDN ), który wyraźnie mówi, że te flagi dają atomowe tworzenie plików. Być może jestem nadmiernie paranoikiem, ale chciałbym zobaczyć słowo kluczowe „atomic”, zanim zaufam mu w przypadku czegokolwiek, co jest krytyczne dla bezpieczeństwa.
me_and
71

Dla porównania, Python 3.3 implementuje nowy 'x'tryb w open()funkcji, aby objąć ten przypadek użycia (tylko tworzenie, niepowodzenie, jeśli plik istnieje). Zauważ, że 'x'tryb jest określony samodzielnie. Używanie 'wx'wyników w a ValueErrorjako, że 'w'jest nadmiarowe (jedyną rzeczą, którą możesz zrobić, jeśli wywołanie się powiedzie, jest i tak zapis do pliku; nie może istnieć, jeśli wywołanie się powiedzie):

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

W przypadku Pythona 3.2 i starszych (w tym Python 2.x) prosimy zapoznać się z zaakceptowaną odpowiedzią .

Dave Jones
źródło
Dobry pomysł. Niestety wydaje się, że jest to tylko POSIX (nie działa w systemie 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
Używasz Pythona 3.2; tryb „x” jest dostępny w wersji 3.3 i nowszych, ale jest to tryb wieloplatformowy. Nawiasem mówiąc, używasz tylko `` x '' zamiast `` wx '' - tryb zapisu jest nadmiarowy, ponieważ jedyną rzeczą, jaką możesz zrobić z plikiem, jest zapis do niego
Dave Jones
Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Szabolcs Dombi
1
Zrobi się - choć będzie to musiało poczekać, aż wrócę trochę później do komputera.
Dave Jones
2
Rozsądne jest otwarcie istniejącego pliku do zapisu, ale głównym celem trybu „x” jest otwarcie pliku wtedy i tylko wtedy, gdy jeszcze nie istnieje , co kończy się błędem, gdy plik istnieje. Dlatego jest zbędny z flagą „w”; jeśli się powiedzie, plik na pewno będzie pusty (a więc nie ma sensu go czytać :).
Dave Jones
0

Ten kod z łatwością utworzy PLIK, jeśli taki nie istnieje.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 
user2033758
źródło
16
Tak, to będzie. Ważnym punktem tego pytania był aspekt bezpieczeństwa. Problem polega na tym, że między zidentyfikowaniem obecności pliku a użyciem go lub jego utworzeniem może się coś zmienić, co spowoduje zły wynik (jak w pierwotnym pytaniu).
Henry Gomersall,
5
To prawda. Nazywa się TOCTOU!
Rad
Jeśli inny proces tworzy i zapisuje do pliku po ifinstrukcji, ten kod spowoduje wyczyszczenie pliku.
Peter Wood,