Tạo tệp một cách an toàn nếu và chỉ khi tệp đó không tồn tại với python


93

Tôi muốn ghi vào một tệp dựa trên việc tệp đó đã tồn tại hay chưa, chỉ ghi nếu nó chưa tồn tại (trong thực tế, tôi muốn tiếp tục thử tệp cho đến khi tôi tìm thấy tệp không tồn tại).

Đoạn mã sau đây cho thấy một cách mà kẻ tấn công tiềm ẩn có thể chèn một liên kết biểu tượng, như được đề xuất trong bài đăng này giữa quá trình kiểm tra tệp và tệp đang được ghi. Nếu mã được chạy với quyền đủ cao, điều này có thể ghi đè lên một tệp tùy ý.

Có cách nào để giải quyết vấn đề này không?

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')

Kiểm tra viết nguyên tử với Python stackoverflow.com/questions/2333872/...
Mikko Ohtamaa

@Mikko Điều đó không giúp được gì ở đây.
Konrad Rudolph

À được rồi. Tôi đã hiểu vấn đề là gì ... bạn CHỈ viết nếu tệp tồn tại?
Mikko Ohtamaa

Bạn có thể ghi tệp vào một vị trí tạm thời, sau đó thực hiện lệnh sao chép mà không cho phép ghi đè?
Eric

Câu trả lời:


93

Chỉnh sửa : Xem thêm câu trả lời của Dave Jones : từ Python 3.3, bạn có thể sử dụng xcờ đểopen() để cung cấp chức năng này.

Câu trả lời gốc bên dưới

Có, nhưng không sử dụng open()lệnh gọi tiêu chuẩn của Python . os.open()Thay vào đó, bạn sẽ cần phải sử dụng , điều này cho phép bạn chỉ định cờ cho mã C bên dưới.

Đặc biệt, bạn muốn sử dụng O_CREAT | O_EXCL. Từ trang người đàn ông cho open(2)dưới O_EXCLtrên hệ thống Unix của tôi:

Đảm bảo rằng lệnh gọi này tạo ra tệp: nếu cờ này được chỉ định cùng với O_CREATvà tên đường dẫn đã tồn tại, thì open()sẽ không thành công. Hành vi của O_EXCLkhông được xác định nếuO_CREAT không được chỉ định.

Khi hai cờ này được chỉ định, các liên kết tượng trưng không được tuân theo: nếu tên đường dẫn là một liên kết tượng trưng, ​​thì open() sẽ không thành công bất kể nơi liên kết tượng trưng trỏ đến.

O_EXCL chỉ được hỗ trợ trên NFS khi sử dụng NFSv3 trở lên trên hạt nhân 2.6 trở lên. Trong các môi trường O_EXCLkhông cung cấp hỗ trợ NFS , các chương trình dựa vào nó để thực hiện các tác vụ khóa sẽ chứa một điều kiện đua.

Vì vậy, nó không hoàn hảo, nhưng AFAIK là cách gần nhất bạn có thể tránh được tình trạng cuộc đua này.

Chỉnh sửa: các quy tắc sử dụng os.open()thay thế khác open()vẫn được áp dụng. Đặc biệt, nếu bạn muốn sử dụng các mô tả tập tin trở lại cho việc đọc hoặc viết, bạn sẽ cần một trong những O_RDONLY, O_WRONLYhoặcO_RDWR cờ là tốt.

Tất cả các O_*cờ đều nằm trong osmô-đun của Python , vì vậy bạn sẽ cần import osvà sử dụng, os.O_CREATv.v.

Thí dụ:

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 cho câu trả lời rõ ràng chính xác. Cá nhân tôi tò mò muốn biết có bao nhiêu người thực sự gặp vấn đề với cảnh báo trước của NFS — tôi (có lẽ liều lĩnh) loại bỏ nó vì một môi trường lỗi thời mà mã của tôi không bao giờ được chạy.
zigg

2
@zigg: NFSv3 có từ năm 1995, vì vậy có vẻ công bằng khi coi các phiên bản cũ là lỗi thời.
Fred Foo

1
Cá nhân tôi sẽ lo lắng hơn về phiên bản hạt nhân. Nếu bạn đang chạy bất cứ thứ gì thậm chí gần giống với một hệ thống cập nhật, bạn sẽ không gặp vấn đề gì, nhưng RHEL 3 (vẫn đang trong giai đoạn hỗ trợ mở rộng) chẳng hạn đang chạy hạt nhân 2.4. Ngoài ra, tôi chưa điều tra xem họ có cung cấp tính năng ghi nguyên tử trên Windows trên FAT hoặc NTFS hay không, đây là một hạn chế lớn tiềm ẩn.
me_and

1
@me_and Trang python về hằng số cờ mở gợi ý rằng điều này hoạt động tốt với Windows. Tôi sẽ thử nó trong thời gian ngắn!
Henry Gomersall

1
Đúng, nhưng tôi chưa thấy bất kỳ nơi nào (kể cả MSDN ) nói rõ ràng những lá cờ này cho phép tạo tệp nguyên tử . Có thể tôi đang bị hoang tưởng quá mức, nhưng tôi muốn xem từ khóa "nguyên tử" đó trước khi tin tưởng điều này cho bất kỳ thứ gì quan trọng về bảo mật.
me_and

69

Để tham khảo, Python 3.3 triển khai một 'x'chế độ mới trong open()hàm để bao gồm ca sử dụng này (chỉ tạo, thất bại nếu tệp tồn tại). Lưu ý rằng 'x'chế độ được chỉ định riêng. Sử dụng 'wx'kết quả trong một ValueErrornhư 'w'là không cần thiết (điều duy nhất bạn có thể làm gì nếu cuộc gọi thành công là ghi vào file anyway; nó không thể đã tồn tại nếu cuộc gọi thành công):

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

Đối với Python 3.2 trở xuống (bao gồm Python 2.x), vui lòng tham khảo câu trả lời được chấp nhận .


Gợi ý tốt. Thật không may này dường như là POSIX-only (không làm việc trên 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
Bạn đang sử dụng python 3.2; chế độ 'x' ở 3.3 trở lên nhưng nó là nền tảng chéo. Ngẫu nhiên, bạn chỉ sử dụng 'x' thay vì 'wx' - chế độ ghi là dư thừa vì điều duy nhất bạn có thể làm với tệp là ghi vào đó
Dave Jones

Python 3.6:ValueError: must have exactly one of create/read/write/append mode
Szabolcs Dombi

1
Sẽ làm - mặc dù sẽ phải đợi cho đến khi tôi quay lại trước máy tính một chút sau đó.
Dave Jones

2
Việc mở một tệp hiện có để ghi là hợp lý, nhưng toàn bộ điểm của chế độ 'x' là mở tệp khi và chỉ khi nó chưa tồn tại , không xảy ra lỗi khi tệp tồn tại. Đây là lý do tại sao nó là thừa với cờ 'w'; nếu nó thành công, tệp được đảm bảo là trống (và do đó có rất ít điểm đọc được từ nó :).
Dave Jones

0

Mã này sẽ dễ dàng tạo một FILE nếu một tệp không tồn tại.

import os
if not os.path.exists('file'):
    open('file', 'w').close() 

15
Nó sẽ được thôi. Điểm quan trọng của câu hỏi là khía cạnh an toàn. Vấn đề là giữa việc xác định sự hiện diện của tệp và sử dụng hoặc tạo tệp, điều gì đó có thể thay đổi dẫn đến kết quả xấu (như trong câu hỏi ban đầu).
Henry Gomersall

5
Đúng. Nó được gọi là TOCTOU!
Rad

Nếu một quá trình khác tạo và ghi vào tệp sau ifcâu lệnh, mã này sẽ xóa tệp.
Peter Wood,
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.