Skip to content

CWE-560: Use of umask() with chmod-style Argument

Overview

umask() sets default permissions for newly created files by naming which permission bits to withhold from the mode the creating call asks for, not which permissions to grant. Developers familiar with chmod's absolute-permission style routinely pass a chmod-style value (e.g. 0600 meaning "I want owner-only") to umask(), which does the opposite of what they intend and creates world-writable files.

Relationship to Other CWEs

  • CWE-560 (this page) - umask called with the wrong value, so the weakness is an argument with the wrong value rather than a missing call
  • CWE-687 (Function Call With Incorrectly Specified Argument Value) - the parent. No page here
  • CWE-628 (Function Call with Incorrectly Specified Arguments) - above CWE-687, carrying the general guidance for arguments of the wrong count, order, type or value. This page is that weakness with one specific call and one specific value
  • CWE-276 (Incorrect Default Permissions) - the condition this page's defect produces on every file the process creates afterwards, so a finding phrased as "files are created world-writable" may be reported under either number. MITRE records no direct relationship between the two, but the fix for the umask case is on this page
  • CWE-732 (Incorrect Permission Assignment for Critical Resource) - where the finding is a specific chmod-style call setting the wrong mode on one named resource, rather than a wrong process-wide default

OWASP Classification

A01:2025 - Broken Access Control

Risk

Medium: An inverted umask creates files that are world-readable or world-writable, so data meant to be private can be read and modified by users who should not have access. The call itself looks unremarkable - every value is a valid mask, so nothing rejects it - and the only evidence is the mode of the files created afterwards, which often goes uninspected until an audit or an incident.

Remediation Steps

Core Principle: umask names the bits to withhold - the kernel clears them from the mode the creating call requests (0666 for files, 0777 for directories). Never pass it the permission value you want; pass it the permissions you want to deny.

Locate Incorrect umask Usage

  • Search for umask() calls and inspect the value passed
  • A value that looks like a normal permission mode (0600, 0644, 0755) is a red flag - those are chmod-style values, not umask-style ones
  • Check the actual permissions of files created after the call (ls -l) - if they are more permissive than intended, the umask value is the likely cause

Understand the Inversion (Primary Defense)

result_permissions = requested_mode AND NOT umask_value

// the creating call requests 0666 (rw-rw-rw-) for files and 0777 (rwxrwxrwx)
// for directories; every bit set in the umask is cleared from that request

// WRONG - passing the desired permission directly, chmod-style
umask(0600)     // intending "owner read/write only"
// actual result: 0666 & ~0600 = 0066 (---rw-rw-) - WORSE than the default, not better!

// RIGHT - passing the bits to remove
umask(0077)     // remove all group/other permission bits
// actual result: 0666 & ~0077 = 0600 (rw-------) - owner-only, as intended

Why this works: umask answers "which bits should never be set", not "what permissions do I want". 0077 means "clear every group and other bit", which leaves exactly the 0600 result wanted. The operation is a bit-clear, not a subtraction: 0666 with 0077 cleared is 0600, whereas subtracting the two numbers would give 0567. Arithmetic agrees with the real answer for some masks and disagrees for others, so read every mask bit by bit.

Common correct mask values:

umask Files Directories Meaning
0077 600 (rw-------) 700 (rwx------) owner only
0027 640 (rw-r-----) 750 (rwxr-x---) owner + group read
0022 644 (rw-r--r--) 755 (rwxr-xr-x) world-readable (traditional Unix default)

Prefer Explicit Permissions at Creation Time Over a Global umask

Where the language or platform can set permissions directly at file-creation time, use that instead of relying on the ambient umask. It states the intended permission at the point of creation, so the result does not depend on process-wide state that other code may have changed. Reserve a restrictive process-wide umask (e.g. 0077) as the default for any creation path that does not specify permissions explicitly.

Save and Restore umask When Temporarily Changing It

// PROBLEM - a temporary change leaks into unrelated file creation afterward
umask(0000)
create_file('public.txt')     // world-writable - intended
create_file('private.txt')    // ALSO world-writable - not intended!

// SOLUTION - save, change, use, restore - ideally via a scoped helper
old_mask = umask(0000)
try:
    create_file('public.txt')     // world-writable - still the intent
finally:
    umask(old_mask)               // always restore, even if creation failed

Why this works, and how far: umask is process-wide state rather than an argument to one call, so it affects every file the process creates until it is changed again. Saving the previous value and restoring it in a finally-equivalent block, or a language's scoped-context construct where one exists, puts the restrictive default back before any later code creates a file, even if the block raised an exception. That is the whole of what it fixes. The mask belongs to the process, not the thread, so during the try block every other thread creating a file gets the widened value too and no restore can help them: the window is concurrent, not sequential. Use this pattern only where file creation during the window is serialised. Anywhere else - and wherever the platform lets the mode be passed to the creating call - set the mode on that one file instead of touching the process-wide mask: it leaves the default untouched and needs no restore. The mask value is the same in both versions above because the widening was never the defect; narrowing it to something like 0022 here would quietly stop producing the world-writable file the caller asked for.

Test File Permissions

  • After the fix, check the actual mode of created files (ls -l, or the platform equivalent). It should match the intended value exactly, rather than merely being more restrictive than before
  • Search for world-writable files that should not exist (find <dir> -perm -002)
  • Verify the process's umask is set to a restrictive value (0077 or 0027) at startup and that nothing widens it afterwards. In a multi-threaded process, a widening that is scoped and restored is not enough on its own: check that no other thread can create a file while it is in effect, or that the public file gets its mode from the creating call instead

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
// Mistake 1: treating umask like chmod
umask(0644)          // intending rw-r--r--
// actual: 0666 & ~0644 = 0022 (----w--w-) - far MORE permissive than intended

// Mistake 2: zero umask "for no restrictions"
umask(0000)
// actual: files get the full default, 0666 (rw-rw-rw-) - world-writable

// Mistake 3: not restoring a temporary umask change
umask(0000)
create_file('public.txt')     // intended to be world-writable
create_file('private.txt')    // also world-writable - the mistake propagates silently

Why this is vulnerable: umask takes the bits to withhold, so its argument is the complement of what a chmod-style reading suggests, and the result is the requested mode with those bits cleared rather than any kind of subtraction. Passing 0644 therefore withholds read and write from the owner, because the mask's leading 6 names both, while its 4s withhold only read from group and other and leave their write bits alone. The result is 0022: a file the owner cannot touch at all and everyone else can write to. That is less usable as well as more dangerous than the intent, which is why the mistake is sometimes reported as a functional bug before anyone treats it as a security issue.

Nothing rejects the argument, because every value is a valid mask. There is no error to check and no warning at any log level, and the call returns the previous mask rather than a status. The only evidence is the permissions on files created afterwards, which nobody inspects unless something has already gone wrong.

The third mistake is the one that spreads, and it is a scope problem rather than an arithmetic one. The mask is process-wide and persists until it is changed again, so a value set for one deliberately public file governs every file the process creates afterwards, including files written by unrelated code in another module. The fix is to set the permission explicitly on the individual file that needs it, which leaves the process-wide mask alone. Saving the previous mask and restoring it in a construct that runs on every exit path is the fallback where the platform gives no per-file option, and it is narrower than it looks: it stops the widened value reaching code that runs after the block, and does nothing for a second thread creating a file during it, because the mask belongs to the process rather than the thread. Use it only where nothing else can be creating files concurrently.

Secure Patterns

// SECURE - pseudo-code
// Restrictive umask set once, correctly, at process startup - the safest of the three,
// because it never widens the mask a concurrent thread might be creating files under
umask(0077)                    // remove all group/other bits -> files land at 0600

// Explicit permissions at creation time, independent of ambient umask
create_file_with_mode('sensitive.dat', mode = 0600)

// Scoped temporary widening, always restored
old_mask = umask(0022)
try:
    create_file('public.txt')  // 0666 & ~0022 = 0644, intentionally shared
finally:
    umask(old_mask)            // restrictive default restored before any later code
                               // in this thread runs - see the note below on other threads

Why this works: The value passed is always "what to withhold", which is what the call does to the requested mode, so the resulting permissions are what was intended rather than the near-opposite. Explicit per-file permissions take ambient process state out of the picture for anything security-sensitive. Scoping a temporary umask change with a guaranteed restore stops a widened default leaking into file creation that happens after the block, even across an exception, but not into another thread's, which runs inside the window and sees the widened mask. In a multi-threaded process, prefer the per-file form above and do not widen the process mask at all.

Common Pitfalls

  • Correcting the umask but leaving the files it already produced: the mask governs creation only, so files written before the fix keep the mode they were created with. Fixing the call and redeploying leaves the existing world-writable files on disk unchanged. They have to be found and re-moded separately, and any secret in them treated as disclosed.
  • Scoping a umask change with finally and calling it contained: the restore bounds the change in time, not in scope. The mask is a property of the process, so while the widened value is in effect every thread creating a file inherits it, and a finally cannot reach back into a window that has already elapsed for another thread. Widening the process mask is safe only where nothing else can be creating files concurrently.
  • Treating a restrictive umask as the control rather than the backstop: the mask is process-wide mutable state that any library or plugin in the same process can change, and that every child process inherits a copy of. It only ever removes bits, so it cannot raise a file above what the creating call requested. For anything that must be owner-only, pass the mode to the call that creates the file and let the umask be the safety net for everything else.
  • Setting it in a wrapper and assuming the service inherited it: a umask line in a shell wrapper applies to that shell and what it starts, but a service restarted by the init system, a re-exec, or a process spawned from a different parent gets whatever that parent had. Set it inside the program, or in the service definition the supervisor actually reads.
  • Reading the mask as an arithmetic difference: checking a proposed value by subtracting it from 0666 agrees with the real result often enough to look reliable, then disagrees. 0666 with 0027 cleared is 0640, where the subtraction gives 0637. Work bit by bit, or create a file and look at the mode.

Additional Resources