CWE-560: Use of umask() with chmod-style Argument
Overview
umask() sets default permissions for newly created files by specifying bits to MASK OFF (subtract from permissions), but developers mistakenly pass chmod-style absolute permissions, creating world-writable files when intending restrictive permissions.
Risk
Medium: Incorrect umask usage creates overly permissive files (world-readable/writable), exposing sensitive data, enabling unauthorized modification, and violating principle of least privilege through permission configuration errors.
Remediation Steps
Core principle: Set file creation permissions correctly: umask disables permission bits - never use chmod-style semantics, and verify resulting modes.
Locate Incorrect umask Usage
When reviewing security scan results:
- Find umask() calls: Search for
umask()function calls in C/Python code - Check values passed: Look for values like 0o600, 0o644 (chmod-style, not mask-style)
- Identify affected files: Which files are created after umask() call
- Check file permissions: Use
ls -lto see actual permissions of created files - Look for overly permissive files: World-readable/writable files (666, 777)
Search patterns:
grep -r "umask(" . --include="*.c" --include="*.py"
grep -r "umask(0o[0-9]" . --include="*.py" # Python octal
find . -perm -002 # Find world-writable files
Understand umask vs chmod (Primary Defense)
# umask REMOVES permissions (inverse of chmod)
# umask specifies bits to MASK OFF from default permissions
# umask 022 means: remove write for group/other
# File default: 666 (rw-rw-rw-)
# Result: 666 - 022 = 644 (rw-r--r--)
# WRONG - passing chmod-style absolute permissions
import os
os.umask(0o600) # Developer thinking "I want rw-------"
# But umask MASKS OFF those bits!
# Result: 666 - 600 = 066 (----rw-rw-)
# Creates WORLD WRITABLE files! Opposite of intent!
# RIGHT - umask masks bits to remove
os.umask(0o077) # Remove ALL group/other permissions
# Result: 666 - 077 = 600 (rw-------)
# Files created with owner-only access (secure)
Why this works: umask is subtractive - it removes permissions from the default. To get owner-only files (600), you must remove group/other permissions (077), not specify 600.
Understanding the math:
# File creation formula:
# actual_permissions = default_mode - umask_value
# Default modes:
# Files: 0o666 (rw-rw-rw-)
# Directories: 0o777 (rwxrwxrwx)
# Common secure umask values:
os.umask(0o077) # Files: 600, Dirs: 700 (owner only)
os.umask(0o027) # Files: 640, Dirs: 750 (owner+group read)
os.umask(0o022) # Files: 644, Dirs: 755 (world readable)
Use Explicit chmod Instead of umask
import os
# BETTER APPROACH - use chmod for explicit control
# Set restrictive umask globally, then chmod specific files
# At application startup:
os.umask(0o077) # Restrictive default for all new files
# Create file with default restrictive permissions
with open('private.txt', 'w') as f:
f.write(sensitive_data)
# File created with 600 (owner only)
# If need specific permissions, use chmod
open('shared.txt', 'w').close()
os.chmod('shared.txt', 0o644) # Explicitly set to rw-r--r--
# For temporary files, use secure APIs
import tempfile
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write(sensitive_data)
temp_path = f.name
# Automatically created with mode 0600
Java example:
import java.nio.file.*;
import java.nio.file.attribute.*;
// Set restrictive permissions when creating file
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
Path file = Files.createFile(Paths.get("secret.txt"), attr);
// File created with 600 permissions
Use Correct umask Values for Security Levels
// C - umask examples for different security needs
// MOST RESTRICTIVE - owner only (recommended for sensitive data)
umask(0077);
// New files: 600 (rw-------)
// New directories: 700 (rwx------)
// GROUP READABLE - owner read/write, group read
umask(0027);
// New files: 640 (rw-r-----)
// New directories: 750 (rwxr-x---)
// TRADITIONAL UNIX DEFAULT - world readable
umask(0022);
// New files: 644 (rw-r--r--)
// New directories: 755 (rwxr-xr-x)
// WRONG - too permissive
umask(0000);
// New files: 666 (rw-rw-rw-) - WORLD WRITABLE!
// Never use this!
Application-specific umask:
# Web application - use restrictive umask
os.umask(0o077) # Only web server user can read
# Database server - group readable for backups
os.umask(0o027) # Owner writes, group reads
# Log files - world readable (if needed for monitoring)
os.umask(0o022) # Everyone reads, owner writes
Save and Restore umask in Local Contexts
import os
from contextlib import contextmanager
# PROBLEM - umask affects all subsequent file creation
os.umask(0o000) # For one public file
open('public.txt', 'w') # World writable - intended
open('private.txt', 'w') # ALSO world writable - NOT intended!
# SOLUTION - save and restore umask
old_umask = os.umask(0o022) # Set for public file
try:
with open('public.txt', 'w') as f:
f.write(public_data)
finally:
os.umask(old_umask) # Restore restrictive umask
# Better - use context manager
@contextmanager
def temporary_umask(mask):
old = os.umask(mask)
try:
yield
finally:
os.umask(old)
# Application default - restrictive
os.umask(0o077)
# Temporarily less restrictive
with temporary_umask(0o022):
with open('public.txt', 'w') as f:
f.write(public_data)
# umask restored to 0o077
with open('private.txt', 'w') as f:
f.write(private_data) # Uses restrictive 0o077
C example:
#include <sys/stat.h>
// Save and restore pattern
mode_t old_mask = umask(0077); // Restrictive
int fd = open("sensitive.dat", O_CREAT|O_WRONLY, 0666);
// File created with permissions: 666 - 077 = 600
close(fd);
umask(old_mask); // Restore original
Test File Permissions
Verification tests:
# Check actual file permissions created
ls -l file.txt
# Expected for sensitive files: -rw------- (600)
# NOT: -rw-rw-rw- (666)
# Find overly permissive files
find /app -type f -perm -002 # World writable
find /app -type f -perm -004 # World readable
find /app -type d -perm -002 # World writable directories
# Check umask value
umask # Should be 0077 or 0027 for security
Test scenarios:
import os
import stat
import tempfile
def test_file_permissions():
# Set restrictive umask
os.umask(0o077)
# Create test file
with tempfile.NamedTemporaryFile(delete=False) as f:
test_file = f.name
# Check permissions
st = os.stat(test_file)
mode = stat.filemode(st.st_mode)
# Should be -rw------- (600)
assert mode == '-rw-------', f"Expected -rw-------, got {mode}"
# Numeric check
perms = st.st_mode & 0o777
assert perms == 0o600, f"Expected 0o600, got {oct(perms)}"
os.unlink(test_file)
def test_umask_calculation():
# Test understanding of umask
os.umask(0o077)
# Files should be: 666 - 077 = 600
os.umask(0o022)
# Files should be: 666 - 022 = 644
# WRONG interpretation
os.umask(0o600) # Don't do this!
# Files would be: 666 - 600 = 066 (world writable!)
Security audit:
- All umask() calls use mask-style values (077, 027, 022)
- No chmod-style values passed to umask (600, 644, etc.)
- Sensitive files have restrictive permissions (600 or 640)
- umask is saved and restored when temporarily changed
- No world-writable files created (find -perm -002 returns none)
- Application sets secure default umask (0o077) at startup
umask Calculation
File creation:
Directory creation:
Common Mistakes
Thinking umask is Like chmod
# MISTAKE 1: Thinking umask is like chmod
os.umask(0o644) # Intending rw-r--r--
# Actually: 666 - 644 = 022 (----w--w-)
# Result: files created with MORE permissions than intended!
Why is this vulnerable: Developers familiar with chmod expect os.umask(0o644) to create files with rw-r--r-- permissions. However, umask is subtractive - it removes bits from the default mode. With default file mode 0666, umask(0o644) calculates 0666 - 0644 = 0022, which only removes write permissions from group/other. This creates files with more permissive access than intended, potentially exposing sensitive data to unauthorized users. The confusion stems from chmod using absolute permissions while umask uses inverted masks.
Using Zero umask
# MISTAKE 2: Using 0 umask
os.umask(0) # Thinking "no restrictions"
# Result: 666 (rw-rw-rw-) - world writable!
Why is this vulnerable: Setting umask(0) removes zero bits from the default mode, meaning files are created with full default permissions: 0666 (rw-rw-rw-) for files and 0777 (rwxrwxrwx) for directories. This makes all newly created files world-writable, allowing any user on the system to modify application data, configuration files, or sensitive information. Attackers with local access can overwrite these files to inject malicious code, escalate privileges, or cause denial of service. Always use a restrictive umask like 0o077 for security.
Not Restoring umask
# MISTAKE 3: Not restoring umask
os.umask(0o000)
open('public.txt', 'w') # Intended for this file
open('private.txt', 'w') # Also world-writable!
Why is this vulnerable: The umask setting is process-wide and persists across all subsequent file creation operations. When developers set a permissive umask for one specific file but forget to restore the previous value, all files created afterward inherit the permissive mask. This causes unintended security issues where sensitive files like database credentials, private keys, or user data become world-writable or world-readable. The bug is particularly dangerous because it may not be noticed immediately - files created later in the application lifecycle silently have incorrect permissions, potentially exposing data for extended periods.
Secure Patterns
Save and Restore umask (C)
mode_t old_mask = umask(0077);
int fd = open("sensitive.dat", O_CREAT|O_WRONLY, 0666);
// File created with permissions 600
umask(old_mask); // Restore original
Why this works: Saving and restoring umask (old_mask = umask(0077); ... umask(old_mask)) ensures the restrictive umask only affects the intended file creation, preventing the umask from leaking to subsequent file operations that may need different permissions. umask(0077) masks off all group and other permissions (removes rwx for group, rwx for other), so files created with mode 0666 become 0600 (0666 - 0077 = 0600 = rw-------), ensuring only the file owner can read/write. Immediate restoration prevents a common bug where setting umask(0077) globally makes all subsequent files owner-only, breaking applications that need shared files. File creation mode 0666 in open() specifies the desired permissions before umask, and umask subtracts the bits to remove - this is correct usage: specify full desired permissions (0666), umask removes unwanted bits (077), result is secure (0600). This pattern is exception-safe in C when wrapped in proper error handling - even if open() fails, the umask is restored, preventing permission leakage.
Context Manager for umask (Python)
from contextlib import contextmanager
@contextmanager
def secure_umask():
old = os.umask(0o077)
try:
yield
finally:
os.umask(old)
with secure_umask():
open('secret.key', 'w').write(key)
Why this works: The context manager pattern (with secure_umask():) provides automatic umask restoration through Python's __enter__/__exit__ protocol - the umask is saved on entry (old = os.umask(0o077)), the code block executes with the secure umask, and the finally block guarantees restoration even if exceptions occur. try/finally ensures umask is always restored whether the file creation succeeds, fails, or raises an exception, preventing umask leakage that would affect subsequent operations. Scoped restrictive permissions - files created inside the with block get mode 0600 (owner-only), while files created outside use the original umask, providing principle of least privilege per operation. Reusable abstraction - instead of remembering to save/restore umask every time, developers use the secure_umask() context manager, reducing errors. Self-documenting code - with secure_umask(): clearly signals intent to create files with restrictive permissions. Python's context managers are exception-safe and work correctly with early returns, continue/break statements, and exception propagation.