Skip to content

fix: store OAuth credentials and config with owner-only file permissions - #4

Merged
cedev-1 merged 2 commits into
mainfrom
devin/1784657536-harden-credential-perms
Jul 21, 2026
Merged

fix: store OAuth credentials and config with owner-only file permissions#4
cedev-1 merged 2 commits into
mainfrom
devin/1784657536-harden-credential-perms

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Security fix from a codebase scan. The client persists OAuth access and refresh tokens in ~/.cloudreve/drives.json, and app settings in the config file. Both were written with std::fs::write, which creates files using the process umask (typically 0644 — world-readable). On shared macOS/Linux machines, any other local user could read the stored tokens.

This PR makes credential/config writes owner-only:

  • New utils::secure_fs helper:
    • write_private(path, contents) — writes then chmod 0600 (Unix); no-op elsewhere.
    • restrict_file / restrict_dir0600 / 0700 on Unix.
  • Applied at every persist site so pre-existing world-readable files are tightened on the next save:
// drive/manager/mod.rs — persist()
- fs::write(&config_file, content)?;               // drives.json (OAuth tokens)
+ secure_fs::write_private(&config_file, content)?;

// config.rs — save()
- fs::write(&self.config_path, content)?;
+ secure_fs::write_private(&self.config_path, content)?;

// dirs created with restrict_dir(...) => 0700

Behavior is unchanged on the happy path; only the on-disk permission bits change. Added unit tests asserting the credential file lands at 0600 and that an existing 0644 file is tightened on rewrite.

Other findings from the scan (not changed here — need a decision)

  • Hardcoded OAuth CLIENT_SECRET in ui/src/utils/constants.ts, shipped in the distributed app. For a native/public client using PKCE the embedded secret provides no real protection and is extractable from any install. Removing it safely requires registering the desktop client as a public PKCE client on the Cloudreve server (dropping client_secret from the token exchange); doing it blindly would break auth, so it's left untouched pending that server-side change.
  • Tokens stored in plaintext on disk (this PR restricts access but does not encrypt). Moving them to the OS keychain / Secret Service would be the stronger fix.
  • csp: null and assetProtocol scope **/* in tauri.conf.json are permissive; tightening them is worthwhile but risks breaking the app (it connects to arbitrary user-configured servers), so it needs testing.
  • SQL access (Diesel, incl. one raw sql_query that uses bind params), the cloudreve:// deep-link handler, and Tauri IPC commands were reviewed and looked fine. A dependency audit (cargo audit, yarn npm audit) couldn't run in this environment — recommend wiring it into CI.

Testing

cargo test -p cloudreve-sync — all pass (31 unit + integration/scenario tests + 2 new secure_fs tests).

Link to Devin session: https://app.devin.ai/sessions/1a95a6aa9b35413d82c31da765c241e4
Requested by: @cedev-1

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@cedev-1 cedev-1 self-assigned this Jul 21, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@cedev-1

cedev-1 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Good direction overall — this is a real security improvement over the current 0644 default. Two things worth fixing before merge:

  1. TOCTOU race in write_private
    The file is currently written with the default umask, then chmod 0600. Between those two operations, another local process could read the file contents. For a desktop app the window is small, but it's easy to fix by creating the file with the right
    mode from the start:

#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut f = std::fs::OpenOptions::new()
.write(true).create(true).truncate(true)
.mode(0o600)
.open(path)
.with_context(|| format!("Failed to open {}", path.display()))?;
f.write_all(contents.as_ref())
.with_context(|| format!("Failed to write {}", path.display()))?;
return Ok(());
}

This eliminates the race entirely — the file never exists with loose permissions.

  1. restrict_dir inconsistency in config.rs
    In manager/mod.rs, restrict_dir is called unconditionally after the if !exists block, so it tightens an already-existing directory that was created before this hardening. In config.rs however, restrict_dir is inside the if !parent.exists() block —
    meaning an existing config directory with 0755 will never be tightened on the next save. Should be moved outside the block to match the behavior in manager/mod.rs.

Apart from these two points, the implementation is solid: the #[cfg(unix)] / #[cfg(not(unix))] split is correct, the tests cover both new-file and tighten-existing-file cases, and calling restrict_dir on startup is a good touch for upgrading old
installs automatically.

This doesn't replace Keychain/secret-service for token storage (which remains the proper long-term fix), but it's a worthwhile stop-gap that's easy to maintain.

— Review assisted by Claude Opus 4.6

@cedev-1 cedev-1 added the enhancement New feature or request label Jul 21, 2026
…fig dir

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Thanks — both points addressed in b3ec85a:

  1. TOCTOU racewrite_private now opens the file with OpenOptions::mode(0o600) on Unix, so it's created with the right mode from the start and never exists world-readable. restrict_file is still called afterward to tighten a pre-existing file (since mode only applies at creation time).
  2. restrict_dir inconsistency — moved outside the if !parent.exists() block in config.rs, so an existing 0755 config dir is tightened on the next save, matching manager/mod.rs.

Agreed that Keychain/Secret Service remains the proper long-term fix for token storage; this is the stop-gap. cargo test -p cloudreve-sync still green.

@cedev-1
cedev-1 merged commit 3ba1047 into main Jul 21, 2026
1 check passed
@cedev-1
cedev-1 deleted the devin/1784657536-harden-credential-perms branch July 26, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant