Skip to content

fix(security): trust check for project-level .rpg.toml (#824 H5) - #831

Merged
NikolayS merged 5 commits into
mainfrom
security/h5-config-trust
Apr 26, 2026
Merged

fix(security): trust check for project-level .rpg.toml (#824 H5)#831
NikolayS merged 5 commits into
mainfrom
security/h5-config-trust

Conversation

@NikolayS

Copy link
Copy Markdown
Owner

Summary

Fixes finding H5 from #824. A .rpg.toml discovered by walking CWD up to $HOME is loaded with no permission or ownership check. Because PROMPT1 and friends flow through backtick-shell expansion in the REPL (src/repl/mod.rs:282), cloning an untrusted repo and starting rpg inside it can execute arbitrary shell on first prompt render.

Threat model

  1. Attacker controls a Git repo (or any directory rpg might be invoked in).
  2. Repo contains a .rpg.toml setting PROMPT1 = "\evil-cmd`db=> "`.
  3. Victim clones the repo and runs rpg. rpg walks up from CWD, finds .rpg.toml, loads it. The REPL renders the prompt and executes evil-cmd via sh -c.

Analogous tools (direnv .envrc, VS Code workspace settings) require explicit trust for exactly this reason. pgpass already has a permission check at src/connection.rs:1566–1582; the far more powerful .rpg.toml did not.

Minimum-viable fix

New check_project_config_trust() in src/config.rs, modelled on the pgpass check:

  • File must be owned by the current uid.
  • Mode must satisfy & 0o022 == 0 (no group/other write).

Untrusted files are skipped with a stderr warning and an actionable chmod 0600 hint; the REPL still starts. User-level config under ~/.config/rpg/ is implicitly trusted and unaffected (matches how psql treats ~/.psqlrc).

The check is #[cfg(unix)]; on Windows the loader proceeds because POSIX modes are not portably exposed — same gating as the pgpass check.

Deferred work

The richer TOFU-style allowlist at ~/.config/rpg/trusted_project_configs (direnv-style opt-in trust) suggested in #824 is out of scope for this PR; it is left as a TODO(#824 H5) in check_project_config_trust's docstring. Implementing this minimum-viable check first lets us ship the fix immediately and design the allowlist UX deliberately.

H6 (env scrubbing on the backtick subshell) is being addressed in a parallel PR.

Test plan

Red/green TDD per CLAUDE.md.

  • Red: config::tests::load_project_config_skips_world_writable_file — creates a temp dir with a world-writable .rpg.toml, points CWD at it, asserts load_project_config() does not load the file. Verified failing on main (the world-writable config was loaded).
  • Green: same test passes after the fix.
  • cargo test --lib --bins — 2064 passed, 0 failed (1 new test added).
  • cargo fmt --check — clean.
  • cargo clippy --all-targets --all-features -- -D warnings — clean.
  • cargo check --target wasm32-unknown-unknown — builds.

Refs #824.

Nik Samokhvalov added 2 commits April 25, 2026 15:19
Adds a unit test that creates a world-writable `.rpg.toml` in a temp dir,
points CWD at it, and asserts `load_project_config()` skips the file.

The test fails on main: today the loader has no permission/ownership check
for project configs, so a world-writable file is silently loaded.  The fix
in the next commit makes this test pass.

Refs #824 H5.
A `.rpg.toml` discovered by walking CWD up to `$HOME` is loaded with no
permission or ownership check.  Because `PROMPT1` and friends flow through
backtick-shell expansion in the REPL (`src/repl/mod.rs:282`), cloning an
untrusted repo and starting rpg inside it can execute arbitrary shell on
first prompt render.

Add `check_project_config_trust()`, modelled on the pgpass check at
`src/connection.rs:1566–1582`:

  * file must be owned by the current uid;
  * mode must satisfy `& 0o022 == 0` (no group/other write).

Untrusted files are skipped with a stderr warning and an actionable
`chmod 0600` hint; the REPL still starts.  User-level config under
`~/.config/rpg/` is implicitly trusted and unaffected.

The check is `#[cfg(unix)]`; on Windows the loader proceeds (POSIX modes
are not portable) — same gating as pgpass.

This is the minimum-viable fix.  A TOFU-style allowlist at
`~/.config/rpg/trusted_project_configs` (direnv-style opt-in) is left as
a TODO under #824 H5.

Refs #824.
Comment thread src/config.rs Fixed
CodeQL's cleartext-logging rule (#831 review) flagged `meta.uid()` and
`geteuid()` flowing into the error string and on to `rpg_eprintln!`.
Same pattern as #817 in `connection.rs`: uid values are sensitive in
CodeQL's taint model and must not reach a logger.

Drop the numeric uids from the error message — the diagnostic value
was low (the user can `ls -l` the file to see ownership) and the
trust check itself is unchanged. The mode error message is reformatted
slightly to keep the chmod hint, which is the actually useful bit.

Existing test (`load_project_config_skips_world_writable_file`) still
passes; assertion is on file rejection, not the error string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS

Copy link
Copy Markdown
Owner Author

Addressed CodeQL cleartext-logging finding (security/code-scanning/140) in 6e5b1a9.

The flagged taint flow was meta.uid() / geteuid()format!rpg_eprintln!. Same shape as #817 fixed in connection.rs last week — uid values are sensitive in CodeQL's taint model.

Fix: drop the numeric uids from the error message. Diagnostic value was low (ls -l shows ownership), and the trust check itself is unchanged. Mode error keeps the chmod 0600 hint, which is the actually useful bit.

Existing test load_project_config_skips_world_writable_file still passes — it asserts on rejection, not the error string.

@NikolayS

Copy link
Copy Markdown
Owner Author

REV review — PR #831

Tight, well-scoped fix for #824 H5: trust check on project .rpg.toml mirroring the pgpass shape, gated #[cfg(unix)], with a real red→green→fix commit sequence and a passing world-writable test.

Findings

  • INFO security (src/config.rs:819–824): POSTGRES.md adjacent to an untrusted .rpg.toml is still read unconditionally and flows into AI context. Not a shell-exec vector and out of scope for H5, but the same hostile-clone threat model applies — consider a follow-up issue.
  • INFO security (src/config.rs:806): std::fs::metadata follows symlinks, so a symlink to attacker-owned content would still face the target file's mode check. Matches pgpass posture (src/connection.rs:1566–1582 is also metadata-based) — consistent. Worth a one-line note in a follow-up audit.
  • INFO security (non-unix): the loader proceeds without any check on Windows/wasm. The TODO mentions the TOFU follow-up; H6's prompt-expansion neutralisation is the actual defence-in-depth on those platforms. Documented adequately for now.
  • NON-BLOCKING bugs (src/config.rs:828): let _ = path; after the #[cfg(unix)] block is a clippy silencer for the non-unix arm. Cleaner pattern: rename the parameter to _path on non-unix via #[cfg_attr(not(unix), allow(unused_variables))]. Cosmetic.
  • INFO security spec: & 0o022 (write-only) is the correct mask for the threat — read access does not leak secrets from a project config, and stricter & 0o077 would reject many real repos with group-readable trees.
  • INFO bugs: only one call site (src/main.rs:748) reaches load_project_config; the only read_to_string of .rpg.toml is inside the post-trust branch. No bypass path.
  • INFO tests: load_project_config_skips_world_writable_file passes; uses an h5_canary named query and asserts both config_path.is_none() and the entry is absent — would not pass without the fix. No happy-path test for a 0o600 file in this PR (a follow-up improvement).
  • INFO guidelines: three commits (ef39044, 0e4615f, 6e5b1a9), Conventional Commits, imperative, < 72 chars; rustdoc on check_project_config_trust cites Comprehensive review: findings across docs, code quality, security, and CI (v0.11.0) #824, the pgpass parallel, the REPL backtick line, and the deferred TOFU TODO.
  • INFO CodeQL: the third commit (6e5b1a9) addresses an automated cleartext-logging finding by dropping the numeric uids from the error string — same shape as the previous fix at fix(security): break cleartext-logging taint chain #817.

Verdict

PASS


End-to-end test evidence

Unit-test evidence

$ cargo test --bins load_project_config_skips_world_writable_file
running 1 test
test config::tests::load_project_config_skips_world_writable_file ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2067 filtered out; finished in 0.00s

Live end-to-end evidence — negative case (world-writable, rejected)

Built a fresh rpg from this branch and ran it from a directory containing a 0o666 .rpg.toml:

$ ls -la /tmp/rpg-h5-evidence/.rpg.toml
-rw-rw-rw-@ 1 nik wheel 125 Apr 25 20:33 /tmp/rpg-h5-evidence/.rpg.toml

$ cd /tmp/rpg-h5-evidence && rpg -h does-not-exist.invalid -d test_db
rpg: warning: ignoring project config "/private/tmp/rpg-h5-evidence/.rpg.toml": group/other writable; run `chmod 0600 /private/tmp/rpg-h5-evidence/.rpg.toml` to fix
rpg: connection to server at "does-not-exist.invalid", port 5432 failed: failed to lookup address information: nodename nor servname provided, or not known

The trust check fires with the exact chmod 0600 <path> remediation hint. Loader falls through to ProjectConfig::default() (no abort) and continues to the connection step.

Live end-to-end evidence — positive case (0o600, loaded)

After fixing perms:

$ chmod 0600 /tmp/rpg-h5-evidence/.rpg.toml
$ ls -la /tmp/rpg-h5-evidence/.rpg.toml
-rw-------@ 1 nik wheel 125 Apr 25 20:33 /tmp/rpg-h5-evidence/.rpg.toml

$ cd /tmp/rpg-h5-evidence && rpg -h does-not-exist.invalid -d test_db
rpg: connection to server at "does-not-exist.invalid", port 5432 failed: ...

No warning — the config was accepted and the loader proceeded without complaint.

@NikolayS
NikolayS merged commit 94ef6e9 into main Apr 26, 2026
26 checks passed
@NikolayS
NikolayS deleted the security/h5-config-trust branch April 26, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants