Proposal: Make file upload permissions respect umask

Hi all,

TL;DR:
Today, upload permissions, when set numerically, are applied verbatim to uploaded files and the folders created to hold them. It is suggested to use the process umask to limit these permissions.

Details:

In unix-like systems, umask is a process-level setting, which helps determine the permissions of new files and folders when they are created. It is a mask of bits, which specifies the permissions to deny by default. As an example, if a process has it set to 0o022, then, by default, new files and directories created by that process will be readable by everyone, but writable only by the process’ user (2 is the write bit, and it is removed from the group and other sets of permissions).

A couple of months ago, we were handling CVE-2026-25674 in the Security Team. The fix, in essence, was to set permissions on folders directly, where older code was relying on umask manipulations. But in the discussions around that fix, it occurred to us that it may be preferable to respect the process umask – something we don’t do today.

Today, the permissions set for uploaded files and the directories created to hold them, are taken from the FILE_UPLOAD_PERMISSIONS and FILE_UPLOAD_DIRECTORY_PERMISSIONS settings, and without regard to the process umask (unless the setting is None).

Making the permissions of saved files subject to umask means that access to these files may become more (but never less) restricted than specified by the permissions. But more importantly, the umask is a parameter that is familiar to sysadmins, and is easier for them to handle than a Django-specific parameter in a Django-and-deployment-specific settings definition.

On the other hand, naive users might be surprised if they set specific permissions, and the files end up created with less permissions.

We think this is a worth-while hardening, but as such, it deserves public discussion. Opinions, suggestions and arguments are more than welcome.

1 Like

I’m not sure I see where this is necessarily helpful.

A process is always capable of changing its own umask. It’s not like the process that initiates the Django process can enforce the umask in a child process that it spawns.

As a result, it makes sense to me that the FILE_UPLOAD_PERMISSIONS and FILE_UPLOAD_DIRECTORY_PERMISSIONS settings override (“trump”, as written in the docs) any previously defined umask.

If this weren’t the case, the person configuring their Django instance may need to both set the umask along with defining the permissions to ensure the proper permissions are applied - which seems like extra & unnecessary work to achieve the desired results.

It’s not clear to me if setting both of those settings to None would mean that the process’ umask would be respected. Is that the case? If so, would a reasonable hardening be to change the default to None? From my understanding that would mean if someone does need to change it, it’s only changed in one place as Ken highlighted.

Thank you both for this.

I feel your replies express a kind of tension, between the beginner/small-project setting where there is a “person configuring their Django instance”, and the larger project some of us envision, where Django-instance configurators are separate from more generalist sysadmins. In the latter case, having the final permissions controlled from only one place, may not be optimal – it may be better for sysadmins to be able to “draw the lines”, and for site admins to still have some freedom to “play within them”. Of course, umask is not a permission enforcement tool, but secure defaults still matter.

For the large majority of small projects, this shouldn’t matter much – the only user which needs to read these files is the one running Django (and the web server), and it is very unlikely for the umask to stop that. This mostly becomes an issue when the files need to become accessible (or inaccessible) to other processes, run under other users on the same machine.

@CodenameTim : Yes, None would set the final permissions according to umask, giving control over to system settings and the sysadmins who set them – probably a reasonable option for those large projects; but it seems like a less-safe default for the simple, small-project case.

First, to be clear, I’m always working from the perspective of a multi-departmental environment. I spent my entire career working for companies with at least 25,000 employees, and segregation of duties was a way of life.

This means that “controls” need to be effective, and not simply defaults that are only functional by convention. Since the process itself is always able to change its own umask, that - by definition - makes it an unacceptable option as a security control, and whatever defaults are applied are irrelevant.

This is handled by restricting the permissions to the parent of the media directory. Directory permissions cascade. To have access to a directory, you need to have access to all directories above it. If the permissions on that parent directory are drwx------, then only the owner of that directory can see the media directory - and everything underneath it. From a management perspective, that is your point of control for the sysadmin and security personnel.

From a practical standpoint, it would be beneficial for Django to honor the process umask when creating folders and files. This is because the fix we published for CVE-2026-25674 includes a vendored version of a fixed makedirs, taken from Python Bug #86533 and its associated PR.

Python implements makedirs via recursive calls to mkdir, using the given mode (and, with the fix above, also parent_mode). In this algorithm, the final permissions for any created directory are determined by both mode and the process umask. In our Django patch, we added an extra call to chmod to preserve the existing semantics where the process umask is ignored:

        os.mkdir(name, mode)
        # PY315: The call to `chmod()` is not in the CPython proposed code.
        # Apply `chmod()` after `mkdir()` to enforce the exact requested
        # permissions, since the kernel masks the mode argument with the
        # process umask. This guarantees consistent directory permissions
        # without mutating global umask state.
        os.chmod(name, mode)

From a maintenance perspective, and following the “least surprising behavior” principle, it would be preferable to adopt the same semantics as Python’s makedirs. This would allow us to remove the vendored code cleanly once the Python fix is included in a release.

1 Like

I think we might be talking about two slightly different situations here(?)

I agree, that when FILE_UPLOAD_PERMISSIONS and FILE_UPLOAD_DIRECTORY_PERMISSIONS are None, the system defaults (with umask) should be applied.

But in this case, the developer is specifying that the defaults should not be used. They’re being explicit regarding the permissions to be applied.

If you apply the umask to those settings, you are potentially creating a surprising situation, in that the files and directories are not going to have the permissions that you have set.

If I set FILE_UPLOAD_PERMISSIONS=0x664, then I expect files to be created with rw-rw-r--. But if my system is being run in an environment with a default umask of 377, I will find those files created with r-------- - which I would consider to be quite a surprise.

To avoid this surprise, I would need to either:

  • Set the umask myself (perhaps calling os.umask(0) either somewhere within the startup cycle for the process or prior to creating any files)

  • Set the umask in the parent process that starts the various Django instances.

Either way, this seems to be unnecessary work when I’ve already set a setting specifying the desired permissions to be applied.

If it is decided to apply the process umask to files created when FILE_UPLOAD_PERMISSIONS is set, then there should also be a setting available to allow the developer to ensure that the umask is appropriate as well.

@KenWhitesell quick cross-check question: when, in your code, you call os.mkdir with for example mode=0x664, what to you expect as the final permission for the newly created folder? (assume cases of default umask but also umask of 377)

Just a minor clarification, the proposal here IMHO is not to explicitly apply umask to files and dirs, but to use the Python’s stdlib primitives to create file and dirs with their semantics.

I would expect the umask to be applied. In this stated case, mode = 0o664 (octal, not hex) masked with 0o377 would create the file (or directory) as r--------. (In the other case you reference a “default umask”, which is dependent upon what that default happens to be - which can be different by distro and user.)

It’s what I would expect to be the difference between directly calling a low-level api (either with being aware of the attending race conditions that I’m responsible for handling, or using chmod to ensure that the permissions are exactly what I need them to be), and a higher level api in a framework where I don’t have that control over the underlying process management.

If I’m creating directories using os.mkdir then I expect to manage the permissions myself.

If I set FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o755, then I expect Django to set the directory permissions to rwxr-xr-x. I don’t expect to also have to call os.chmod. (If I need to call os.chmod to get my desired result anyway, what is the purpose / value of FILE_UPLOAD_DIRECTORY_PERMISSIONS?)

That’s good to know, thanks.

2 Likes