store: make the database, its WAL and lease files owner-only - #9
Conversation
SQLite sets no mode of its own, so open_sqlite left the file mode to the caller's umask and the shipped default was world-readable 0644. Measured on a real deployment: 11 of 11 module stores at 0644, including cortexkit-credentials. A crate that already decides WAL mode, busy timeout and foreign keys has taken responsibility for how the file behaves on disk. Leaving permissions to callers means the decision is made by the ambient umask, which is to say not made at all. A group-readable store is not a configuration this crate supports anyway: it hands out an exclusive single-writer lease, so an out-of-band reader is already outside the contract. The exposure, stated honestly rather than as an implied multi-user threat: a single-account host has no other human to read these files. What 0644 does expose them to is every process running as this user — every module, every worker, every tool — and anything that copies the tree: a backup, a restore, an install into a shared location, a container bind-mount. THE WAL IS THE HALF THAT GETS MISSED. Recently committed rows live there until a checkpoint, so protecting only the database leaves the newest data readable while the database file itself reads as correct. On the measured host the WALs are routinely larger than their databases — alfonso-core's is 23MB. Lease files are hardened too, in cortexkit-lease where they are created. Their exposure is integrity rather than privacy: the lease carries the persisted epoch that is the single-writer fence token, so a writable lease file lets a stale writer's fence be forged. Applied on OPEN rather than only at creation, because every store already deployed was created at 0644 — a creation-time-only fix protects exactly the installations with no history. A path that is not a regular file is refused rather than adjusted: following a symlink would chmod a file the caller never named, which is a privilege-escalation primitive wearing a hardening step's clothes. Each assertion is mutation-proved separately. Worth recording that the first version of the WAL test could not fail: it used a FIRST open, where SQLite creates the WAL inheriting the database's already-corrected mode. Dropping '-wal' from the protected suffixes passed it. The test now reopens a store with a leftover permissive WAL — the state an unclean shutdown leaves behind, and the only one where a permissive WAL can be waiting at open time.
There was a problem hiding this comment.
2 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/cortexkit-lease/src/lib.rs">
<violation number="1" location="crates/cortexkit-lease/src/lib.rs:79">
P1: A malicious lease-path symlink can still cause an unintended target to be created or chmodded: the callers open the path before this helper, and the helper stats then chmods it in separate syscalls. Open with no-follow and apply permissions to the same validated file handle before any create side effect.</violation>
<violation number="2" location="crates/cortexkit-lease/src/lib.rs:83">
P1: On Windows, `protect_file` always returns `Ok(())` without changing permissions or rejecting symlinks, so the owner-only database, WAL, and lease guarantee is absent on a supported build target. Implement Windows ACL/handle protection or explicitly scope this contract to Unix.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| )); | ||
| } | ||
| if metadata.permissions().mode() & 0o777 != 0o600 { | ||
| std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; |
There was a problem hiding this comment.
P1: A malicious lease-path symlink can still cause an unintended target to be created or chmodded: the callers open the path before this helper, and the helper stats then chmods it in separate syscalls. Open with no-follow and apply permissions to the same validated file handle before any create side effect.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/cortexkit-lease/src/lib.rs, line 79:
<comment>A malicious lease-path symlink can still cause an unintended target to be created or chmodded: the callers open the path before this helper, and the helper stats then chmods it in separate syscalls. Open with no-follow and apply permissions to the same validated file handle before any create side effect.</comment>
<file context>
@@ -35,6 +35,55 @@ use std::{
+ ));
+ }
+ if metadata.permissions().mode() & 0o777 != 0o600 {
+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
+ }
+ }
</file context>
| } | ||
| } | ||
| #[cfg(not(unix))] | ||
| let _ = path; |
There was a problem hiding this comment.
P1: On Windows, protect_file always returns Ok(()) without changing permissions or rejecting symlinks, so the owner-only database, WAL, and lease guarantee is absent on a supported build target. Implement Windows ACL/handle protection or explicitly scope this contract to Unix.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/cortexkit-lease/src/lib.rs, line 83:
<comment>On Windows, `protect_file` always returns `Ok(())` without changing permissions or rejecting symlinks, so the owner-only database, WAL, and lease guarantee is absent on a supported build target. Implement Windows ACL/handle protection or explicitly scope this contract to Unix.</comment>
<file context>
@@ -35,6 +35,55 @@ use std::{
+ }
+ }
+ #[cfg(not(unix))]
+ let _ = path;
+ Ok(())
+}
</file context>
SQLite sets no mode of its own, so
open_sqliteleft the file mode to the caller's umask and the shipped default was world-readable0644. Measured on a real deployment: 11 of 11 module stores at 0644, includingcortexkit-credentials.A crate that already decides WAL mode, busy timeout and foreign keys has taken responsibility for how the file behaves on disk. Leaving permissions to callers means the decision is made by the ambient umask, which is to say not made at all. A group-readable store is not a configuration this crate supports anyway: it hands out an exclusive single-writer lease, so an out-of-band reader is already outside the contract.
The exposure, stated honestly. A single-account host has no other human to read these files. What
0644does expose them to is every process running as this user — every module, every worker, every tool — and anything that copies the tree: a backup, a restore, aninstallinto a shared location, a container bind-mount.The WAL is the half that gets missed. Recently committed rows live there until a checkpoint, so protecting only the database leaves the newest data readable while the database file itself reads as correct. On the measured host the WALs are routinely larger than their databases —
alfonso-core's is 23MB.Lease files too, hardened in
cortexkit-leasewhere they are created. Their exposure is integrity rather than privacy: the lease carries the persisted epoch that is the single-writer fence token, so a writable lease file lets a stale writer's fence be forged.Two deliberate choices:
Verification
Each assertion is mutation-proved separately: dropping
""from the protected suffixes fails the database assertion, dropping"-wal"fails the WAL assertion, and removing theprotect_filecall fromacquirefails the lease test.Worth recording that the first version of the WAL test could not fail. It used a first open, where SQLite creates the WAL inheriting the database's already-corrected mode, so dropping
-walfrom the protected suffixes passed it. The test now reopens a store with a leftover permissive WAL — the state an unclean shutdown leaves behind, and the only one in which a permissive WAL can be waiting at open time.Full workspace:
cargo fmt,cargo clippy --workspace --all-targets -D warnings, andcargo test --workspaceall clean. Verified downstream inck-plexus, which consumes this crate by path: its gate and all 19 conformance fixtures pass, and its own store-permission test fails when theprotect_fileloop is removed here — a cross-repo mutation proof that the rule reaches consumers.Found while auditing
ck-plexus, whosetrigger_eventstable now retains vendor response bodies at rest. SUBC ruled the rule belongs here rather than in each module; the plexus-local implementation has been deleted in favour of this one.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Force owner-only (0600) permissions for SQLite database, WAL/SHM, and lease files to prevent unintended access by other processes. Applied on open so existing deployments are fixed immediately.
{db, -wal, -shm}after enabling WAL incortexkit-storeviaprotect_filefromcortexkit-lease.Written for commit 3418285. Summary will update on new commits.