fix(security): create secret files atomically with owner-only permissions (#1264)#1267
Conversation
…ions (#1264) WriteRestrictedFile creates with FileMode.CreateNew + FileShare.None and the restrictive mode/DACL supplied at creation (Unix UnixCreateMode 0600; Windows protected owner-only FileSecurity passed to CreateFile), then writes through that handle. Closes the create-then-restrict window where a racer's pre-opened handle on the empty temp file survived the tightened ACL. PersistValue and the corrupt-config backup both use it; the backup keeps a recovery-first fallback (plain copy + best-effort restrict) so an ACL failure never costs the only copy of a recoverable key.
New: born-restricted assert (both platforms), CreateNew refuses a pre-existing file without deleting it, quarantine backup is restricted + byte-faithful. The atomic-move test now mirrors the new PersistValue sequence (WriteRestrictedFile then File.Move).
There was a problem hiding this comment.
Code Review
This pull request improves security when writing sensitive configuration files by introducing atomic file creation with owner-only permissions (WriteRestrictedFile), closing a race condition window where files were briefly readable by other local users. Corresponding unit tests were added to verify atomic lockdown, refusal of pre-existing files, and backup restriction. The reviewer suggested a performance improvement to use Encoding.UTF8.GetBytes instead of instantiating a new UTF8Encoding object on every call to avoid unnecessary allocations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…es (#1267 review) CreateFileW silently ignores the supplied security descriptor on non-ACL filesystems (FAT32/exFAT/some SMB) and open(2)'s mode is ignored on non-POSIX mounts -- where the pre-#1264 restrict calls FAILED. Restore the fail-closed contract through the open handle: Windows reads the DACL back (handle opened with ReadPermissions) and refuses an unprotected result; Unix pins exactly 0600 via SetUnixFileMode(SafeFileHandle) -- also umask-proof. Both clean up their own file on a failed lockdown. Also: corrupt-backup fallback copies to a distinct .fallback name (primary residue can no longer block it), honest 'primary backup:' error label, CS0419 cref disambiguated.
…eview) The behavioral tests cannot distinguish atomic create-with-permissions from a regression to create-then-restrict (identical final state on NTFS -- probe-proven by the review). Replace the now-vacuous PersistValue_SetsUnixFileMode_StructuralCheck with checks pinning WriteRestrictedFile wiring (temp + backup), creation-time permissions, the banned old sequence, and the two fail-closed post-create guards.
Adversarial Code ReviewTwo independent review agents (filesystem-security API semantics + failure-paths/test-regression), both empirical: they built scratch net8.0 probes (Windows on this box; Unix in WSL) rather than assuming API behavior. CRITICAL / HIGH
MEDIUM
LOW
Bot Comments Addressed
Summary0 CRITICAL / 0 HIGH / 2 MEDIUM / 3 LOW + 1 informational. MEDIUMs merge-blocking until fixed — all fixed, see follow-up. |
Adversarial Review — Fixes Applied
All findings addressed. CI status: PENDING on |
Closes #1264 (seeded from the #1263/#1241 review; the Copilot bot round on #1263 flagged the same race).
What
New
FirstRunBootstrapper.WriteRestrictedFilecreates the file atomically with owner-only permissions andFileShare.None, then writes through that same handle:FileStreamOptions.UnixCreateMode = UserRead | UserWrite— the file is born0600.FileSecurityis passed toCreateFileitself via the in-boxFileInfo.Create(FileMode, FileSystemRights, FileShare, int, FileOptions, FileSecurity)extension.FileMode.CreateNewrefuses to adopt a file someone pre-created at the path (and the helper never deletes a file it did not create).Why
The previous sequence (
File.Create→RestrictFileToCurrentUser→ write) left a window where the empty temp file existed with the directory's default permissions; a racer could pre-open a read handle that survives the tightened ACL (ACL changes don't revoke open handles) and read the later-written JWT secret / connector key. Unreachable in the single-user deployment, but the claim 'the secret is never in an unprotected file' is now true at every instant.Call sites
PersistValuetemp file →WriteRestrictedFile(tempPath, payload)(+ existing atomicFile.Move).PreserveCorruptConfig.corrupt-*backup → atomic restricted create fromFile.ReadAllBytes(byte-faithful; no copy-then-restrict window), with a recovery-first fallback to the old copy + best-effort restrict so an ACL failure never costs the only copy of a recoverable key.RestrictFileToCurrentUseris retained for forward remediation of EXISTING files (RestrictExistingLocalConfigFile, the backup fallback); its Windows DACL construction is extracted into a sharedBuildOwnerOnlyFileSecurity.Tests (42/42 FirstRun-filtered Api + 13/13 Cli, both green locally; each CI OS covers its platform branch)
WriteRestrictedFile_CreatesFileAtomicallyLockedToCurrentUser— born-restricted (0600 / protected single-SID DACL) + exact content.WriteRestrictedFile_RefusesToAdoptAPreExistingFile—CreateNewthrowsIOException, pre-existing file untouched.QuarantineCorruptLocalConfigAt_BackupIsRestrictedToCurrentUser— backup restricted + byte-faithful.WriteRestrictedFile_LockdownSurvivesAtomicMove— the fix(security): lock appsettings.local.json secrets to the current user on Windows (#1241) #1263 move-preservation test updated to mirror the new PersistValue sequence.Out of scope
CliFirstRunBootstrappersibling (same gap + Unix TOCTOU) — tracked in CliFirstRunBootstrapper: owner-only Windows ACL + fix write-before-chmod TOCTOU on the connector key file #1262.