Skip to content

🔒 [security] Secure atomic file creation with restrictive permissions#391

Open
jpoehnelt wants to merge 1 commit intomainfrom
fix-security-atomic-write-permissions-4706751054827797349
Open

🔒 [security] Secure atomic file creation with restrictive permissions#391
jpoehnelt wants to merge 1 commit intomainfrom
fix-security-atomic-write-permissions-4706751054827797349

Conversation

@jpoehnelt
Copy link
Member

🎯 What: The vulnerability fixed
Secure atomic file creation by using restrictive 0600 permissions (owner-only read/write) for temporary files during the write process.

⚠️ Risk: The potential impact if left unfixed
On Unix-like systems, if the user's umask is permissive (e.g., 0002 or 0022), temporary files created for sensitive data (like OAuth tokens or encrypted credentials) would be world-readable or group-readable until the final permission correction. This creates a window of opportunity for local attackers to read secrets.

🛡️ Solution: How the fix addresses the vulnerability
The atomic_write and atomic_write_async functions in src/fs_util.rs were modified to use OpenOptions instead of fs::write. On Unix platforms, the mode(0o600) extension is called to ensure that the file is created with the correct restrictive permissions from the start. Additionally, sync_all() (or sync_all().await) is called to ensure data is durable on disk before renaming the temporary file to its final destination.

Includes a new unit test on Unix to verify that the files are indeed created with 0o600 permissions.


PR created automatically by Jules for task 4706751054827797349 started by @jpoehnelt

Use `0o600` permissions (read/write only by the owner) when creating
temporary files in `atomic_write` and `atomic_write_async`.

This prevents a race condition on Unix systems where a sensitive file
(like credentials or tokens) could be created with default permissions
(e.g., world-readable) before its permissions are corrected.

The fix uses `OpenOptions::mode(0o600)` on Unix and also adds `sync_all()`
for better data durability.

A new test `test_atomic_write_permissions` verifies the fix on Unix.
A changeset file is also included.

Co-authored-by: jpoehnelt <3392975+jpoehnelt@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@changeset-bot
Copy link

changeset-bot bot commented Mar 10, 2026

🦋 Changeset detected

Latest commit: 9b49bd6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@googleworkspace-bot googleworkspace-bot added the area: core Core CLI parsing, commands, error handling, utilities label Mar 10, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a security vulnerability by enhancing the atomic_write and atomic_write_async functions. The primary goal is to prevent a window of opportunity for local attackers to read sensitive data from temporary files by ensuring these files are created with restrictive 0600 permissions from the outset. Additionally, data durability is improved by explicitly synchronizing file contents to disk before finalization.

Highlights

  • Enhanced Security for Atomic Writes: Temporary files created during atomic write operations (atomic_write and atomic_write_async) now explicitly use restrictive 0600 permissions on Unix systems, preventing unauthorized access to sensitive data before the final file is in place.
  • Improved Data Durability: The sync_all() function is now called on temporary files before renaming them, ensuring that all data is flushed to disk and durable, reducing the risk of data loss during system crashes.
  • Permissions Verification Test: A new unit test was introduced specifically for Unix platforms to confirm that atomic_write and atomic_write_async correctly create temporary files with the intended 0o600 permissions.
Changelog
  • .changeset/security-fix-atomic-write-permissions.md
    • Added a new changeset entry for the security fix related to atomic write permissions.
  • src/fs_util.rs
    • Modified atomic_write and atomic_write_async to use OpenOptions for creating temporary files with 0o600 permissions on Unix.
    • Ensured data durability by calling sync_all() on temporary files before renaming.
    • Added a new unit test test_atomic_write_permissions to verify the correct file permissions on Unix systems.
Activity
  • PR created automatically by Jules for task 4706751054827797349 started by @jpoehnelt.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@googleworkspace-bot googleworkspace-bot added the cla: yes This human has signed the Contributor License Agreement. label Mar 10, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses a security vulnerability by setting restrictive permissions on temporary files during atomic writes. However, a critical security vulnerability has been identified: the current implementation uses predictable temporary filenames and does not ensure exclusive file creation. This leaves the application vulnerable to symlink attacks and potential permission bypasses if an attacker pre-creates the temporary file. To fully address these concerns, it is recommended to use unique, non-predictable filenames and ensure exclusive file creation, for example, by using the O_EXCL flag (via create_new(true)). The implementation for both synchronous and asynchronous functions is otherwise sound, and the accompanying test effectively verifies the initial fix.

Comment on lines +43 to +56
{
use std::fs::OpenOptions;
use std::io::Write;
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(&tmp_path)?;
file.write_all(data)?;
file.sync_all()?;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The implementation of atomic writes uses a predictable temporary filename (<filename>.tmp) and opens it without the O_EXCL flag (via create_new(true)). This presents two security risks:

  1. Symlink Attack: An attacker can create a symbolic link at the predictable temporary path, causing the application to follow the link and write to an arbitrary file.
  2. Permission Bypass: If an attacker pre-creates the temporary file with permissive permissions (e.g., 0666), the application will reuse the existing file and its permissive permissions, allowing the attacker to read sensitive data. This undermines the security fix intended by this PR.

To remediate this, use a unique, non-predictable temporary filename and ensure it is created exclusively. The recommended approach is to use the tempfile crate's NamedTempFile or Builder to create a temporary file with a random suffix in the target directory. If a predictable name must be used, ensure create_new(true) is used and handle the case where the file already exists by either failing or using a different name. Alternatively, you could generate a semi-random name, for example by including a timestamp and process ID:

use std::time::{SystemTime, UNIX_EPOCH};

let pid = std::process::id();
let rand_part = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let tmp_name = format!("{}.{}-{}.tmp", file_name.to_string_lossy(), pid, rand_part);

This change should be applied where tmp_name is defined.

Comment on lines +73 to +86
{
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use tokio::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(&tmp_path).await?;
file.write_all(data).await?;
file.sync_all().await?;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The implementation of atomic writes uses a predictable temporary filename (<filename>.tmp) and opens it without the O_EXCL flag (via create_new(true)). This presents two security risks:

  1. Symlink Attack: An attacker can create a symbolic link at the predictable temporary path, causing the application to follow the link and write to an arbitrary file.
  2. Permission Bypass: If an attacker pre-creates the temporary file with permissive permissions (e.g., 0666), the application will reuse the existing file and its permissive permissions, allowing the attacker to read sensitive data. This undermines the security fix intended by this PR.

To remediate this, use a unique, non-predictable temporary filename and ensure it is created exclusively. The recommended approach is to use the tempfile crate's NamedTempFile or Builder to create a temporary file with a random suffix in the target directory. If a predictable name must be used, ensure create_new(true) is used and handle the case where the file already exists by either failing or using a different name. For asynchronous functions, while the tempfile crate is ideal, its primary API is synchronous and might require tokio::task::spawn_blocking for integration. Alternatively, you could generate a semi-random name, for example by including a timestamp and process ID:

use std::time::{SystemTime, UNIX_EPOCH};

let pid = std::process::id();
let rand_part = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let tmp_name = format!("{}.{}-{}.tmp", file_name.to_string_lossy(), pid, rand_part);

This change should be applied where tmp_name is defined.

@github-actions github-actions bot added the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core CLI parsing, commands, error handling, utilities cla: yes This human has signed the Contributor License Agreement. gemini: reviewed Gemini Code Assist has reviewed the latest changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants