56

I'm trying to create a file that is only user-readable and -writable (0600).

Is the only way to do so by using os.open() as follows?

import os
fd = os.open('/path/to/file', os.O_WRONLY, 0o600)
myFileObject = os.fdopen(fd)
myFileObject.write(...)
myFileObject.close()

Ideally, I'd like to be able to use the with keyword so I can close the object automatically. Is there a better way to do what I'm doing above?

Asclepius
  • 57,944
  • 17
  • 167
  • 143
lfaraone
  • 49,562
  • 17
  • 52
  • 70

6 Answers6

41

What's the problem? file.close() will close the file even though it was open with os.open().

with os.fdopen(os.open('/path/to/file', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
  handle.write(...)
Matt
  • 5,028
  • 2
  • 28
  • 55
vartec
  • 131,205
  • 36
  • 218
  • 244
  • 6
    I consider this answer better than mine, but it is not "what is the problem": you present a new factor the OP was notaware of -that is the convertion of a file handler into a Python File object – jsbueno Apr 11 '11 at 16:55
  • @jsbueno: I've just combined first two lines together and used `with`. And in the example in the question the file being closed via `myFileObject.close()` anyway. – vartec Apr 11 '11 at 16:59
  • 1
    This doesn't work for me. os.open with these flags expects the file to already exist.>>> f = os.open('test.txt', os.O_WRONLY, 0600) Traceback (most recent call last): File "", line 1, in OSError: [Errno 2] No such file or directory: 'test.txt' – Ian Goodfellow Aug 30 '12 at 20:49
  • @stair314, that's probably because you didn't specify `O_CREAT`. See current version of answer. – Asclepius Feb 21 '13 at 23:50
  • 2
    @vartec, there is a `umask` specific problem with this answer. I have posted an answer to address this concern. – Asclepius Feb 22 '13 at 02:15
34

This answer addresses multiple concerns with the answer by vartec, especially the umask concern.

import os
import stat

# Define file params
fname = '/tmp/myfile'
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL  # Refer to "man 2 open".
mode = stat.S_IRUSR | stat.S_IWUSR  # This is 0o600.
umask = 0o777 ^ mode  # Prevents always downgrading umask to 0.

# For security, remove file with potentially elevated mode
try:
    os.remove(fname)
except OSError:
    pass

# Open file descriptor
umask_original = os.umask(umask)
try:
    fdesc = os.open(fname, flags, mode)
finally:
    os.umask(umask_original)

# Open file handle and write to file
with os.fdopen(fdesc, 'w') as fout:
    fout.write('something\n')

If the desired mode is 0600, it can more clearly be specified as the octal number 0o600. Even better, just use the stat module.

Even though the old file is first deleted, a race condition is still possible. Including os.O_EXCL with os.O_CREAT in the flags will prevent the file from being created if it exists due to a race condition. This is a necessary secondary security measure to prevent opening a file that may already exist with a potentially elevated mode. In Python 3, FileExistsError with [Errno 17] is raised if the file exists.

Failing to first set the umask to 0 or to 0o777 ^ mode can lead to an incorrect mode (permission) being set by os.open. This is because the default umask is usually not 0, and it will be applied to the specified mode. For example, if my original umask is 2 i.e. 0o002, and my specified mode is 0o222, if I fail to first set the umask, the resulting file can instead have a mode of 0o220, which is not what I wanted. Per man 2 open, the mode of the created file is mode & ~umask.

The umask is restored to its original value as soon as possible. This getting and setting is not thread safe, and a threading.Lock must be used in a multithreaded application.

For more info about umask, refer to this thread.

Asclepius
  • 57,944
  • 17
  • 167
  • 143
  • 1
    Many thanks for addressing the `umask` issue, sorted out my problem. – Nobilis Oct 31 '14 at 09:26
  • 1
    Thanks for this answer! I couldn't figure out why every file I saved from python got 0600 file permissions, even when using ``os.open(mode=0666)``. This resolved the issue. – ostrokach Feb 20 '15 at 02:07
  • 1
    This answer puzzled me a bit for referencing XOR. umask doesn't XOR. However setting the umask to 0 is correct if you want to be sure the permissions of the file are exactly as specified and not something with fewer bits set because of the user's umask. If your goal is to just ensure the file is not world readable/writeable, you don't need to set the umask. – Nelson Feb 18 '16 at 15:38
  • @Nelson Fixed. Removed XOR. The goal is generic, i.e. to have a function that can write a file with the arbitrarily specified parametrized permissions. – Asclepius Oct 21 '16 at 08:34
12

update Folks, while I thank you for the upvotes here, I myself have to argue against my originally proposed solution below. The reason is doing things this way, there will be an amount of time, however small, where the file does exist, and does not have the proper permissions in place - this leave open wide ways of attack, and even buggy behavior.
Of course creating the file with the correct permissions in the first place is the way to go - against the correctness of that, using Python's with is just some candy.

So please, take this answer as an example of "what not to do";

original post

You can use os.chmod instead:

>>> import os
>>> name = "eek.txt"
>>> with open(name, "wt") as myfile:
...   os.chmod(name, 0o600)
...   myfile.write("eeek")
...
>>> os.system("ls -lh " + name)
-rw------- 1 gwidion gwidion 4 2011-04-11 13:47 eek.txt
0
>>>

(Note that the way to use octals in Python is by being explicit - by prefixing it with "0o" like in "0o600". In Python 2.x it would work writing just 0600 - but that is both misleading and deprecated.)

However, if your security is critical, you probably should resort to creating it with os.open, as you do and use os.fdopen to retrieve a Python File object from the file descriptor returned by os.open.

Asclepius
  • 57,944
  • 17
  • 167
  • 143
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 1
    I know that is not the question but the same code is useful to set file owner and group on the fly just change chmod line for os.chown(name, uid, gid). – alemol Oct 12 '16 at 17:17
4

The question is about setting the permissions to be sure the file will not be world-readable (only read/write for the current user).

Unfortunately, on its own, the code:

fd = os.open('/path/to/file', os.O_WRONLY, 0o600)

does not guarantee that permissions will be denied to the world. It does try to set r/w for the current user (provided that umask allows it), that's it!

On two very different test systems, this code creates a file with -rw-r--r-- with my default umask, and -rw-rw-rw- with umask(0) which is definitely not what is desired (and poses a serious security risk).

If you want to make sure that the file has no bits set for group and world, you have to umask these bits first (remember - umask is denial of permissions):

os.umask(0o177)

Besides, to be 100% sure that the file doesn't already exist with different permissions, you have to chmod/delete it first (delete is safer, since you may not have write permissions in the target directory - and if you have security concerns, you don't want to write some file where you're not allowed to!), otherwise you may have a security issue if a hacker created the file before you with world-wide r/w permissions in anticipation of your move. In that case, os.open will open the file without setting its permissions at all and you're left with a world r/w secret file...

So you need:

import os
if os.path.isfile(file):
    os.remove(file)
original_umask = os.umask(0o177)  # 0o777 ^ 0o600
try:
    handle = os.fdopen(os.open(file, os.O_WRONLY | os.O_CREAT, 0o600), 'w')
finally:
    os.umask(original_umask)

This is the safe way to ensure the creation of a -rw------- file regardless of your environment and configuration. And of course you can catch and deal with the IOErrors as needed. If you don't have write permissions in the target directory, you shouldn't be able to create the file, and if it already existed the delete will fail.

jytou
  • 510
  • 4
  • 7
  • After removing the file as you suggested, an ideal value for `flags` may be `os.O_WRONLY | os.O_CREAT | os.O_EXCL`. Note that `os.O_EXCL` prevents the opening of a file that already exists. – Asclepius Oct 21 '16 at 09:49
  • _"the code: ... does not guarantee that permissions will be denied to the world. It does guarantee that the file will have r/w for the current user, that's it!"_ - This is false. The permissions supplied to `os.open()` will be set (after masking with umask) as provided when creating the file. `0o600` does mean that the file will be `go-rwx`. It does _not_ mean that the file is `u+rw` (because umask). See the [`open(2)` man page](https://linux.die.net/man/2/open) for details. What might be tripping you up is that the mode _only_ works on file creation; it is ignored if the file already exists. – marcelm Mar 14 '18 at 13:49
  • @marcelm yes, you are right (I was too much in my context where I set umask beforehand), I fixed it, thanks! – jytou Mar 15 '18 at 23:09
1

I would like to suggest a modification of A-B-B's excellent answer that separates the concerns a bit more clearly. The main advantage would be that you can handle exceptions that occur during opening the file descriptor separately from other problems during actual writing to the file.

The outer try ... finally block takes care of handling the permission and umask issues while opening the file descriptor. The inner with block deals with possible exceptions while working with the Python file object (as this was the OP's wish):

try:
    oldumask = os.umask(0)
    fdesc = os.open(outfname, os.O_WRONLY | os.O_CREAT, 0o600)
    with os.fdopen(fdesc, "w") as outf:
        # ...write to outf, closes on success or on exceptions automatically...
except IOError, ... :
    # ...handle possible os.open() errors here...
finally:
    os.umask(oldumask)

If you want to append to the file instead of writing, then the file descriptor should be opened like this:

fdesc = os.open(outfname, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)

and the file object like this:

with os.fdopen(fdesc, "a") as outf:

Of course all other usual combinations are possible.

András Aszódi
  • 8,948
  • 5
  • 48
  • 51
  • Appending an existing file makes no sense in this context because the `mode` specified to `os.open` will then never be applied. The previously existing mode will preserve. – Asclepius Oct 21 '16 at 09:37
1

I'd do differently.

from contextlib import contextmanager

@contextmanager
def umask_helper(desired_umask):
    """ A little helper to safely set and restore umask(2). """
    try:
        prev_umask = os.umask(desired_umask)
        yield
    finally:
        os.umask(prev_umask)

# ---------------------------------- […] ---------------------------------- #

        […]

        with umask_helper(0o077):
            os.mkdir(os.path.dirname(MY_FILE))
            with open(MY_FILE, 'wt') as f:
                […]

File-manipulating code tends to be already try-except-heavy; making it even worse with os.umask's finally isn't going to bring your eyes any more joy. Meanwhile, rolling your own context manager is that easy, and results in somewhat neater indentation nesting.

ulidtko
  • 14,740
  • 10
  • 56
  • 88