Apply Restrictive File Permissions

Files should be created with restrictive file permissions to prevent vulnerabilities such as information disclosure and code execution. In particular, any files which may contain confidential information should be set to only permit access by the owning user/service and group (i.e. no world/other access).

Discretion should be used when granting write access to files such as configuration files to prevent vulnerabilities including denial of service and remote code execution.

Incorrect

Consider the configuration file for a service “secureserv” which stores configuration including passwords in “secureserv.conf”.

ls -l secureserv.conf
-rw-rw-rw-   1 secureserv  secureserv   6710 Feb 17 22:00 secureserv.conf

Here the file permissions are set to 666 (read and write access for owner, group, and others). This will allow all users on the system to have access to the sensitive information contained within the configuration file.

When writing to a file on a *NIX operating system using Python, the file output will be written using the umask that is set for the application. Depending on your operating system configuration this could be too permissive when writing sensitive data to file.

Given this program:

with open('testfile.txt', 'w') as fout:
    fout.write("secrets!")

The file permissions will default to the environment settings. Here the umask allows file content to be read by other users on the system.

ls -l testfile.txt
-rw-r--r--  1 user  staff  4 Feb 19 10:59 testfile.txt

Correct

It is preferable to set restrictive permissions for files containing sensitive information. To fix the example above you would need to set permissions to make the file only readable and writeable by the user that created it.

chmod 0600 securesev.conf
ls -l secureserv.conf
-rw-------   1 secureserv  secureserv   6710 Feb 17 22:00 secureserv.conf

Below is an example of how you could securely create a file in Python which is only readable and writable by the owner of that file.

import os

flags = os.O_WRONLY | os.O_CREAT | os.EXLC
with os.fdopen(os.open('testfile.txt', flags, 0o600), 'w') as fout:
    fout.write("secrets!")

Note that it is also important to verify the owner and group of the file. It is particularly important to note which other users are part of a group that you grant access to. The best practice is that if group access is not needed, do not grant it. This is the principle of least privilege.

Testing guide

It should be possible to test anything that performs file creation to make sure that permissions are being set correctly. The following example inspects that the file is created with expected permissions and that an exception is raised if the file already exists.

import stat
import os
import unittest


def do_something(path, mode):
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    return os.fdopen(os.open(path, flags, 0o600), mode)

class TestFileCreation(unittest.TestCase):

    def test_correct_permissions(self):
        """
        Make sure that a file can be created with specific permissions
        """
        test_file = "demo.txt"
        with do_something(test_file, "w") as fout:
            fout.write("blah blah blah")

        finfo = os.stat(test_file)
        assert(finfo.st_mode & stat.S_IRUSR)
        assert(finfo.st_mode & stat.S_IWUSR)
        assert(not finfo.st_mode & stat.S_IXUSR)
        assert(not finfo.st_mode & stat.S_IRGRP)
        assert(not finfo.st_mode & stat.S_IWGRP)
        assert(not finfo.st_mode & stat.S_IXGRP)
        assert(not finfo.st_mode & stat.S_IROTH)
        assert(not finfo.st_mode & stat.S_IWOTH)
        assert(not finfo.st_mode & stat.S_IXOTH)

        os.remove(test_file)

    def test_file_exists(self):
        """
        If the file already exists an OSError should be raised.
        """
        test_file = "demo2.txt"

        # simulate file already placed at location by attacker
        with open(test_file, "w") as fout:
            fout.write("nasty attacker stuff")

        # ensure that we can't open the file
        with self.assertRaises(OSError):
            with do_something(test_file, "w") as fout:
                fout.write("this should never happen..")

        os.remove(test_file)

You can also use mock to ensure that any calls to os.open are made with restrictive permissions.

import os
import mock
import unittest

def do_something(dummy_file):
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    with os.fdopen(os.open(dummy_file, flags, 0o600), "w") as fout:
        fout.write("blah blah")

    print("doing something")


class TestUsingMock(unittest.TestCase):

    def test_do_something(self):
        """
        Make sure that any file created is done with sufficiently secure permissions.
        """
        with mock.patch('os.open') as os_open, mock.patch('os.fdopen') as os_fdopen:

            # setup test call
            dummy_file = "dummy.txt"
            os_open.return_value = 123
            do_something(dummy_file)

            # ensure the file is being created with specific flags
            # and permission set
            flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
            os_open.assert_called_with(dummy_file, flags, 0o600)
            os_fdopen.assert_called_with(123, "w")

Consequences

  • Reading passwords from the config file - A malicious user can read sensitive information (such as passwords) from the file.
  • Setting a new password - A malicious user can write a new password into the file, potentially granting access.
  • Code execution - If the config file stores commands or parameters, a malicious user could tamper with the config file to achieve code execution.
  • Denial of service - An attacker could delete the contents of the file to prevent the service from running properly.

References

  • File system controls should be implemented according to least privilege.
  • More information about setting securing file system permissions in Linux can be found here.