Conversation
|
🔍 OpenCodeReview found 5 issue(s) in this PR.
📄
|
| Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | ||
| std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; | ||
| Ok(false) | ||
| } |
There was a problem hiding this comment.
The AlreadyExists branch unconditionally resets permissions to 0600 on every open. This silently repairs pre-existing files (good for upgrading old installations), but also silently overrides any intentional relaxation by the user (e.g., group-readable for backup tooling) on each launch. Consider documenting this repair-on-open behavior, or only repairing when permissions are world-readable, so user intent other than the insecure default is preserved.
Suggestion:
| Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | |
| std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; | |
| Ok(false) | |
| } | |
| Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | |
| // Only repair when the file is exposed to other users, so an intentionally | |
| // relaxed (but still private-ish) configuration is not silently overwritten. | |
| let mode = std::fs::metadata(path)?.permissions().mode(); | |
| if mode & 0o077 != 0 { | |
| std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; | |
| } | |
| Ok(false) | |
| } |
| #[cfg(unix)] | ||
| create_private_file(path).map_err(error::process_io_error)?; |
There was a problem hiding this comment.
The security hardening is Unix-only. On non-Unix platforms (e.g., Windows), SQLite will still create the database file with default permissions, so wallet data may be readable by other local users there. If other platforms are supported, consider equivalent hardening (e.g., Windows ACL restriction) or at least document the known gap in the function's doc comment.
Suggestion:
| #[cfg(unix)] | |
| create_private_file(path).map_err(error::process_io_error)?; | |
| // TODO(hardening): on non-Unix platforms the file is created by SQLite with | |
| // default permissions; add equivalent ACL hardening or document the gap. | |
| #[cfg(unix)] | |
| create_private_file(path).map_err(error::process_io_error)?; |
|
Addressed the OpenCodeReview findings (run) in bb948a7: Adopted:
Declined:
|
| match std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path) { | ||
| Ok(_file) => Ok(true), |
There was a problem hiding this comment.
Two gaps: (1) .mode(0o600) is ANDed with the process umask, so the resulting mode depends on the environment — unlike the directory case, no explicit set_permissions enforces 0600. (2) SQLite itself creates the rollback journal / WAL / SHM files next to the DB with umask-influenced default permissions (e.g. 0644); the pre-created DB file does not protect them. In the documented pre-existing-directory case (directory left at 0755), those sidecar files are world-readable while containing sensitive data. Consider an explicit set_permissions(0o600) after creation, and documenting/mitigating the sidecar-file exposure.
Suggestion:
| match std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path) { | |
| Ok(_file) => Ok(true), | |
| let file = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path); | |
| match file { | |
| Ok(_file) => { | |
| // `mode` is masked by the umask; enforce 0600 explicitly for umask independence. | |
| std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; | |
| Ok(true) | |
| } |
Wallet databases contain highly sensitive data: the root extended private key, the root VRF private key, and (optionally) the BIP-39 mnemonic and passphrase in plaintext. With the common 0022 umask, the database file was created as 0644 and missing parent directories as 0755, allowing another local unprivileged user (when the path is traversable) to copy the database and recover the signing authority. - Create missing parent directories as 0700, which also protects the auxiliary Sqlite files (rollback journal, WAL, shared-memory, temporary files) under the same private directory boundary. Permissions of pre-existing directories are left untouched, since they may be shared with unrelated data. - Create the database file atomically as 0600 (pre-creating it before Sqlite opens it), so its contents are never observable with looser permissions, even momentarily. - Repair the database file permissions to 0600 when opening an existing database created by a pre-fix version. Unix only; other platforms keep the previous behavior.
Replace the racy exists() check with an atomic create_dir call: if the leaf is created by this call it is tightened to 0700, if it already existed its permissions are left untouched as documented. Pin the latter behavior with a dedicated test.
bb948a7 to
f869714
Compare
| std::fs::create_dir_all(dir)?; | ||
| true | ||
| } | ||
| Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => false, | ||
| Err(err) => return Err(err), | ||
| }; | ||
|
|
||
| if created { | ||
| std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?; | ||
| } |
There was a problem hiding this comment.
When intermediate parent components are missing, create_dir_all creates them with the process umask (potentially group/world-readable), and only the leaf is tightened to 0700. Sensitive intermediate directories on the wallet path (e.g. ~/.wallet//) can remain readable by others. Additionally there is a small TOCTOU window between directory creation and set_permissions(0700) during which the new leaf has umask permissions. Consider creating components one at a time with create_dir + immediate set_permissions, or tightening every component this call created.
Suggestion:
| std::fs::create_dir_all(dir)?; | |
| true | |
| } | |
| Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => false, | |
| Err(err) => return Err(err), | |
| }; | |
| if created { | |
| std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?; | |
| } | |
| // Walk components so every directory this call creates gets 0700 immediately. | |
| let mut prefix = None; | |
| for component in dir.components() { | |
| let mut next = std::path::PathBuf::new(); | |
| if let Some(p) = &prefix { | |
| next.push(p); | |
| } | |
| next.push(component); | |
| std::fs::create_dir(&next).ok(); | |
| let _ = std::fs::set_permissions(&next, std::fs::Permissions::from_mode(0o700)); | |
| prefix = Some(next); | |
| } |
Fixes a High-severity local vulnerability (CWE-732, suggested CVSS 3.1: 7.8) in wallet database creation.
Issue
Wallet databases contain highly sensitive material: the root extended private key, the root VRF private key, and — when
store-seed-phraseis enabled — the complete BIP-39 mnemonic and optional passphrase, in plaintext. With the common0022umask:0755(create_dir_all)0644Any other local unprivileged user could copy a traversable database and recover full signing authority without the victim's RPC credentials.
Fix (
storage/sqlite, Unix)0700— this also shields the auxiliary Sqlite files (rollback journal, WAL, shared-memory, temporary files) under the same private directory boundary. Permissions of pre-existing directories are left untouched, since they may be shared with unrelated data.0600(pre-created viaOpenOptions::create_new().mode(0o600)before Sqlite opens it), so it is never observable with looser permissions, even momentarily.0600.Non-Unix platforms keep the previous behavior.
Tests
Two new Unix-only tests in
storage/sqlite:0700, file06000644file → repaired to0600on openVerification
cargo test -p storage-sqlite: 27/27 ✓,cargo test -p wallet-controller: 24/24 ✓,cargo test -p wallet: 114/114 ✓./do_checks.shgreen under the CI toolchain (1.92.0)