Skip to content

Add project configuration trust verification (trust-project-config) - #6365

Draft
swissspidy wants to merge 26 commits into
mainfrom
add/trust-project-config
Draft

Add project configuration trust verification (trust-project-config)#6365
swissspidy wants to merge 26 commits into
mainfrom
add/trust-project-config

Conversation

@swissspidy

@swissspidy swissspidy commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

Adds a project configuration trust mechanism (--trust-project-config / WP_CLI_TRUST_PROJECT_CONFIG) so that a project-level wp-cli.yml cannot execute arbitrary code just because someone cds into the directory and runs wp.

What is gated

Trust is verified for the directives that a project wp-cli.yml introduces on top of the system/global configuration:

  • require — loads a PHP file
  • exec — runs PHP code
  • env — sets WP-CLI environment variables such as WP_CLI_PACKAGES_DIR, excluding an allowlist of inert keys (WP_ENV, WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY)
  • ssh-args, including ssh-args on an alias, which can smuggle a ProxyCommand

Benign keys (path, url, user, …) are not gated. A project config that introduces none of the above is untouched.

How trust is granted

Checked in order, in Utils\check_project_config_trust():

  1. --trust-project-config on the command line (true, false, or a path/list of paths).
  2. trust-project-config in the system or global config file. This value is snapshotted before the project config is merged, so a project cannot grant itself trust by setting trust-project-config: true in its own wp-cli.yml.
  3. The trust store at ~/.wp-cli/trusted-configs.json, which maps a canonical config path to the SHA-256 hash of its contents. A previously trusted file that has since been modified is re-prompted rather than silently trusted.
  4. The WP_CLI_TRUST_PROJECT_CONFIG environment variable, same accepted values as the flag.
  5. An interactive [y/n/a] prompt on a TTY. a writes the path and content hash to the trust store.

Outside a TTY, an untrusted project config is a hard error rather than a prompt — including under --yes, which deliberately does not grant trust.

Implementation notes

  • Verification is centralized in Runner::init_config() rather than in the individual bootstrap steps, so it covers env/ssh-args/alias directives that are consumed before LoadExecCommand and LoadRequiredCommand run. It happens after runtime arguments are parsed so that --trust-project-config is honored.
  • WP_CLI::warning() and WP_CLI::error() fall back to STDERR when they are called before the logger is initialized, which is the case for the trust check.
  • Attacker-controlled paths and directive contents are escaped before being written to the terminal.

Tests

Unit tests in tests/UtilsTest.php and acceptance scenarios in features/config.feature.

Note: this needs wp-cli/wp-cli-tests#348 to be released before CI can go green — Behat fixtures create project wp-cli.yml files with require directives all the time, and a non-interactive test run cannot answer the trust prompt.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f88766f-4314-4c06-bffd-ce798db3c571

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

WP-CLI now tracks project and global exec/require directives, validates project configuration trust through flags and environment settings, persists approved paths in global YAML, and gates directive execution. Feature and unit tests cover trust outcomes and configuration handling.

Changes

Project Config Trust

Layer / File(s) Summary
Trust configuration and scope tracking
php/config-spec.php, php/WP_CLI/Runner.php
Adds the repeatable trust-project-config option and tracks exec, require, and trust settings across configuration scopes.
Trust validation and persistence
php/utils.php, tests/UtilsTest.php
Updates trust messages and YAML locking/list handling, with tests for explicit approvals, environment settings, empty directives, and persisted trust.
Directive execution gating and scenarios
php/WP_CLI/Bootstrap/..., features/config.feature, features/cli.feature
Gates project-only exec and require directives and updates coverage for disabled, enabled, runtime-flag, self-authorization, persisted-trust, and parameter-dump behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: scope:framework

Suggested reviewers: schlessera, copilot, brianhenryie

Sequence Diagram(s)

sequenceDiagram
  participant ProjectConfig
  participant LoadExecCommand
  participant LoadRequiredCommand
  participant TrustCheck
  participant GlobalConfig
  ProjectConfig->>LoadExecCommand: provide project exec directives
  ProjectConfig->>LoadRequiredCommand: provide project require directives
  LoadExecCommand->>TrustCheck: validate project exec trust
  LoadRequiredCommand->>TrustCheck: validate project require trust
  TrustCheck->>GlobalConfig: persist approved project path
  TrustCheck-->>LoadExecCommand: allow or reject exec evaluation
  TrustCheck-->>LoadRequiredCommand: allow or reject required files
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding project configuration trust verification through the trust-project-config option.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add/trust-project-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

coderabbitai Bot and others added 2 commits July 29, 2026 21:37
Fixed 3 file(s) based on 5 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

… and transactional locking for config updates
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.85938% with 113 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
php/utils.php 59.49% 64 Missing ⚠️
php/WP_CLI/Runner.php 30.15% 44 Missing ⚠️
php/class-wp-cli.php 0.00% 3 Missing ⚠️
php/WP_CLI/Bootstrap/LoadExecCommand.php 0.00% 1 Missing ⚠️
php/WP_CLI/Bootstrap/LoadRequiredCommand.php 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings August 3, 2026 15:04

This comment was marked as resolved.

swissspidy and others added 5 commits August 3, 2026 18:30
- Defer the trust check until after runtime config parsing so that an
  explicit `--trust-project-config` flag is honored.
- Derive the reported directive label (exec/require/generic) from the
  gated directives instead of always using the generic label.
- Remove an orphaned docblock in utils.php.
- Do not truncate the trust store when json_encode() fails.
- Escape the config path in all user-facing messages and include it in
  the non-interactive error.
- Treat an empty WP_CLI_TRUST_PROJECT_CONFIG as unset.
- Surface the "allow always" confirmation when no logger is set up yet.
- Move the gated directive collection out of the bare config block so the
  indentation matches the coding standard.
- Predeclare the system/global snapshots so PHPStan can see them.
- Harden load_trusted_configs() against a malformed trust store.
- Add trust-project-config to the wp-cli.yml JSON schema.
@swissspidy swissspidy modified the milestone: 3.0.0 Aug 5, 2026
The project-config trust gate only flagged aliases whose *name* was new
(`array_diff_key` against the system/global aliases). A project wp-cli.yml
could therefore redefine an existing, commonly-named alias (e.g. `@prod`)
to add `ssh-args: -oProxyCommand=...`, and the injected SSH option would
execute locally when the operator invoked that alias — without ever
tripping the trust prompt.

Compare ssh-args per alias instead: gate any project alias whose ssh-args
is new or differs from the same-named alias in the trusted system/global
config. An existing alias with unchanged ssh-args stays trusted, so this
adds no new prompts for already-trusted configurations.

Adds a regression scenario covering redefinition of a global alias.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LV1bNtxNCZ3QXujJHYfhZv

@swissspidy swissspidy left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the trust mechanism end-to-end from a security-audit angle. Overall this is a solid, well-structured design: snapshotting global_trust_config from system/global config before the project merge correctly prevents a project config from self-authorizing (nicely covered by the dedicated scenario); the store is content-hashed and 0600; the interactive prompt control-char-escapes the attacker-controlled path and directives; and non-interactive contexts default to deny. 👍

I pushed one commit fixing a gap I found (squash/adjust/drop as you like):

Alias ssh-args gating missed redefinition of an existing alias. The gate used array_diff_key($project_aliases, $global_aliases), which only flags aliases whose name is new. But add_alias() overwrites an existing entry, so a project wp-cli.yml could redefine a commonly-named existing alias (@prod, @staging, …) to add ssh-args: -oProxyCommand=… and slip past the gate — then execute locally when the operator ran wp @prod …. Existing new-alias tests still pass. The commit switches to a per-alias comparison (gate when ssh-args is new or differs from the same-named system/global alias) and adds a regression scenario.

Three things I did not change, for your call:

  1. Trust hash covers only wp-cli.yml, not the files it require:s. Once a config is trusted, editing a required PHP file without touching wp-cli.yml runs the new code without re-prompting — same model as direnv (.envrc vs. sourced files). Likely acceptable; worth a doc note.
  2. Redefining an existing alias's connection target (not ssh-args) isn't gated. A project config can repoint @prod's ssh: host; validate_ssh_bits() blocks option-injection but not a plain hostname swap, so wp @prod … could silently connect to an attacker's host (using the operator's key). Not code-exec and lower severity, but it's the same "redefine a trusted alias" class as the ssh-args gap — you may want to gate any project redefinition of an existing alias.
  3. BC note for the changelog: in non-interactive contexts (CI), existing users with require:/exec: in a project wp-cli.yml now hit a hard error until they set WP_CLI_TRUST_PROJECT_CONFIG=true or allowlist the path. That's the intended secure-by-default behavior — just worth calling out prominently.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants