fix(security): P0-1 config/permissions trust model — deny-by-default project config + grant provenance#103
Merged
Merged
Conversation
…po-local trust (P0-1) Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
… claim (P0-1 review) Closes whole-branch review I-1 (tilde env_file wrongly filtered), I-2 (list env_file with a cwd-relative entry stayed trusted), M-1 (overclaimed "never bricks the app"). Claude-Session: https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes P0-1 (Critical) from the 2026-07-10 comprehensive review: repository-controlled config and permission grants formed a host-RCE chain. A cloned/untrusted repo's
./.{app}/settings.jsoncould select the stateful (host) executor backend, container image/user, bind mounts, outputs dir, OS-sandbox policy, and raw LLM logging (onlypermissions_enabledwas stripped); and its./.{app}/permissions.local.jsonallow-grants loaded as trusted and could be force-tracked into the repo. This PR establishes an explicit trust model: user/admin config (env vars,~/.{app}) controls security boundaries; project config is untrusted. 6 commits; offline suite1941 passed, 0 failed.What's fixed
Half 1 — deny-by-default project settings (
config.py). The project./.{app}/settings.jsonand a cwd-relative.envare now restricted to an explicit allowlist_PROJECT_SETTABLE_KEYSof 23 benign fields (model/behavior, retry & request timeouts, sandbox resource limits, non-exec tool config,session_store, display/logging verbosity) via a wrapping_AllowlistFilterSourcethat keeps only allowlisted keys and logs a warning per dropped key (drops, never raises). Everything else —stateful_executor_backend,sandbox_image/sandbox_container_user/sandbox_data_mounts/sandbox_outputs_dir,os_sandbox_*,skill_scripts_enabled,skills_dirs,shell_sandbox_type/shell_docker_image,raw_llm_logging,workspace_dir, permission rules, secrets — is dropped. Real environment variables and the user~/.{app}/settings.jsonstay fully trusted. The.envguard inspects everyenv_fileentry withexpanduser()and filters if any is cwd-relative (fail-safe over-filter); an absolute /~-user-level / all-absolute-listenv_filestays trusted.Half 2 — grant provenance (
settings_persistence.py,permissions/store.py,permissions/engine.py). Interactive "Allow always" grants now persist to a user-side~/.{app}/project_grants.jsonkeyed by the resolved project path (was: repo-local./.{app}/permissions.local.json). The engine loads only the current project's entry (trusted allow+deny) and no longer loads the repo-local file at all;get_project_local_permissions_pathis removed. A clone at a different path carries no grants (re-grant on first use). No migration. The DENY-only projectsettings.jsonload is preserved (a repo can still tighten policy).Threat model
The untrusted parties are (a) a cloned/malicious repository's project-local config and (b) model-controlled data. Neither can now flip a security boundary or pre-authorize a tool. Verified end-to-end: a repo shipping sensitive
settings.json+ apermissions.local.jsonallow-rule has the sensitive settings dropped and the allow-rule untrusted, so a gated tool call still reaches the approval prompt.Verification
1941 passed, 1 skipped (gated LaTeX), 26 xfailed, 0 failed. New tests are host-side (real temp dirs, isolated$HOME, real settings/.env/grant files):tests/test_config_trust.py(allowlist +.envtrust tiers) andtests/permissions/test_grant_provenance.py(grant relocation, clone-path isolation, chain blocked).~/...env_file was wrongly filtered — pydantic expands~, butPath.is_absolute()doesn't), I-2 (a listenv_filewith a cwd-relative entry stayed trusted — fail-open), M-1 (a "never bricks the app" overclaim). The 23-key allowlist was verified against the live model — the design spec's illustrative names were corrected to the real fields.Not in scope (separate cycles)
orchestratorremains project-settable (both backends enforce permissions; flagged by the review, kept per the approved spec).https://claude.ai/code/session_01SwkKXQpy3JLEqrPkQA8QRv