
Preventing Unsafe File Permissions in Rust: Securely Creating and Managing Sensitive Files
Why file permissions matter
File permissions control who can read, write, or execute a file. For security-sensitive applications, the default behavior of the operating system is not always safe enough.
Typical risks include:
- Secrets exposed to other users: API keys, session tokens, and private keys written to files with mode
0644can be read by anyone on the system. - Tampering by untrusted users: if a writable file is also readable or modifiable by others, attackers may alter configuration or state.
- Race conditions during creation: creating a file first and restricting permissions later can leave a window where the file is accessible.
- Insecure directories: even if a file is private, placing it in a shared directory can leak its name or allow replacement attacks.
The core principle is simple: set restrictive permissions at creation time, not after the fact.
Common secure defaults
For sensitive files, these are good starting points:
| Artifact | Recommended permissions | Notes |
|---|---|---|
| Private key file | 0600 | Owner read/write only |
| Token cache | 0600 | Avoid shared caches for secrets |
| User-specific config with secrets | 0600 or 0640 | Use 0640 only when group access is intentional |
| Private directory | 0700 | Prevent directory listing and file replacement |
| Shared read-only config | 0644 | Only for non-sensitive data |
On Unix-like systems, permissions are usually represented as octal modes. On Windows, the model is different, so you should rely on platform-appropriate APIs and avoid assuming Unix semantics everywhere.
Create files securely on Unix
In Rust, the standard library lets you set permissions after creating a file, but that is not ideal for security-sensitive data. A safer approach is to create the file with restrictive permissions from the start.
Example: create a private file atomically
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
fn write_secret_file(path: &Path, contents: &[u8]) -> io::Result<()> {
let mut file = OpenOptions::new()
.create_new(true) // fail if the file already exists
.write(true)
.mode(0o600) // owner read/write only
.open(path)?;
file.write_all(contents)?;
file.sync_all()?; // ensure data is flushed to disk
Ok(())
}
fn main() -> io::Result<()> {
let path = Path::new("/home/alice/.config/myapp/token");
write_secret_file(path, b"super-secret-token")?;
Ok(())
}Why this is safer
create_new(true)prevents overwriting an existing file..mode(0o600)requests restrictive permissions at creation time.sync_all()reduces the chance of data loss after a crash.
If you instead create the file and then call set_permissions, there is a brief period where the file may be accessible with broader permissions.
Restrict directory permissions too
A private file in a public directory is still a problem. Attackers may not be able to read the file directly, but they can often enumerate names, replace files, or exploit symlink-based mistakes.
Create private directories with 0700:
use std::fs;
use std::io;
use std::os::unix::fs::DirBuilderExt;
use std::path::Path;
fn create_private_dir(path: &Path) -> io::Result<()> {
let mut builder = fs::DirBuilder::new();
builder.mode(0o700);
builder.create(path)
}Use this for locations such as:
~/.config/your-app/~/.local/share/your-app/- application-specific state directories containing secrets
If the directory is shared, a malicious local user may still manipulate filenames or exploit assumptions in your code.
Avoid permission changes after creation
A common anti-pattern is:
- Create the file.
- Write sensitive data.
- Change permissions afterward.
That sequence is risky because the file may be briefly visible with default permissions inherited from the process umask or filesystem defaults.
Prefer this pattern
- Create the file with the correct mode.
- Write the contents.
- Flush and close it.
- Only then continue with dependent operations.
If you must update permissions on an existing file, do it only when the file is not sensitive or when you can prove it was already private.
Understand umask, but do not rely on it alone
On Unix systems, the process umask masks permission bits during file creation. For example, if your code requests 0666 but the umask is 0022, the resulting file may become 0644.
That sounds helpful, but relying on umask is fragile:
- It is process-wide, not local to one file.
- It may be changed by parent processes or shell startup scripts.
- It does not express your security intent clearly.
A better approach is to request restrictive permissions explicitly in code. You can still use a conservative umask as defense in depth, but do not treat it as your primary control.
Use temporary files carefully
Temporary files are often used to stage secrets, generate certificates, or write atomic updates. These files must be private and created safely.
Safer temporary file workflow
- Create the file in a private directory.
- Use
create_new(true)or a secure temp-file API. - Write the data.
- Rename it into place atomically if needed.
For example, when updating a secret file:
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
fn atomic_write_secret(target: &Path, data: &[u8]) -> io::Result<()> {
let parent = target.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "missing parent directory")
})?;
let tmp_path = parent.join(".token.tmp");
{
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
.open(&tmp_path)?;
file.write_all(data)?;
file.sync_all()?;
}
fs::rename(&tmp_path, target)?;
Ok(())
}This pattern reduces the risk of partially written files and avoids exposing the final path until the write is complete.
Compare secure approaches
| Goal | Recommended approach | Avoid |
|---|---|---|
| Create a secret file | OpenOptions::create_new(true) with mode(0o600) | Create first, restrict later |
| Create a private directory | DirBuilderExt::mode(0o700) | Shared directories for secrets |
| Update sensitive content atomically | Write temp file privately, then rename | In-place truncation of live secret files |
| Prevent accidental overwrite | create_new(true) | create(true) without existence checks |
This table is a useful checklist when reviewing code that touches credentials or private state.
Handle existing files defensively
Sometimes your application must read or update a file that already exists. In that case, verify permissions before trusting the contents.
On Unix, you can inspect metadata and reject files that are too permissive:
use std::fs;
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn ensure_private_file(path: &Path) -> io::Result<()> {
let metadata = fs::metadata(path)?;
let mode = metadata.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("insecure permissions: {:o}", mode),
));
}
Ok(())
}This check ensures that group and other bits are not set. You can use a similar approach for directories, requiring 0700 or at least no group/other access.
Important caveat
Permission checks are useful, but they are not a substitute for secure creation. An attacker may race your check if you later reopen the file by path. Whenever possible, open the file securely first and keep the handle open.
Be careful with symlinks and path reuse
Permission mistakes often combine with symlink attacks. If your code writes to a predictable path in a writable directory, another user may replace that path with a symlink to a different file.
To reduce risk:
- Use private directories.
- Use
create_new(true)for new files. - Avoid writing to predictable names in shared locations.
- Prefer atomic rename workflows over direct writes to the final path.
If your application runs with elevated privileges, these precautions become even more important.
Cross-platform considerations
Rust code often targets Unix and Windows, but the permission model differs.
Unix-like systems
- Permissions are represented as mode bits.
OpenOptionsExtandDirBuilderExtlet you request modes directly.PermissionsExtlets you inspect and modify modes.
Windows
- Access control is based on ACLs rather than Unix mode bits.
- The standard
mode()APIs do not map cleanly to Windows security semantics. - For sensitive Windows applications, use platform-specific ACL configuration or a crate that supports secure ACL setup.
A practical strategy is to design your code so that:
- Unix uses restrictive mode bits.
- Windows uses secure directory locations and ACL-aware setup.
- Shared logic does not assume that
0600means the same thing everywhere.
Best practices for secure file handling
Do
- Create sensitive files with restrictive permissions from the start.
- Use
create_new(true)when the file should not already exist. - Place secrets in private directories with
0700. - Prefer atomic rename for updates.
- Validate existing permissions before trusting user-controlled files.
- Flush data to disk when durability matters.
Do not
- Write secrets to files with default permissions.
- Rely on
chmod-style fixes after creation. - Store secrets in shared temp directories.
- Overwrite existing files unless that behavior is intentional.
- Assume Unix permission bits behave the same on Windows.
A practical checklist
Before shipping code that writes files, ask:
- Is the file sensitive?
- Is the directory private?
- Are permissions set at creation time?
- Can the file be overwritten by an attacker?
- Is the write atomic?
- Are platform differences handled explicitly?
If you can answer “yes” to the first four and “yes” to the last two where relevant, your file-handling code is likely much safer.
Conclusion
Unsafe file permissions are a subtle but serious security issue in Rust applications. The language protects you from memory corruption, but it does not automatically protect secrets on disk. The safest pattern is to create private directories, create files atomically with restrictive permissions, and avoid post-creation fixes that leave a vulnerability window.
When your code handles credentials, keys, or sensitive state, treat file permissions as part of the security boundary. Secure defaults are not optional; they are part of correct behavior.
