Skip to content

Use 0755 instead of 0700 for the packages directory - #244

Merged
swissspidy merged 2 commits into
mainfrom
claude/wp-cli-security-audit-t5dmuw
Aug 4, 2026
Merged

Use 0755 instead of 0700 for the packages directory#244
swissspidy merged 2 commits into
mainfrom
claude/wp-cli-security-audit-t5dmuw

Conversation

@swissspidy

@swissspidy swissspidy commented Aug 4, 2026

Copy link
Copy Markdown
Member

Follow-up to #243, which set the packages directory to 0700.

The problem

0700 breaks installations that point WP_CLI_PACKAGES_DIR at a location shared between users — e.g. a host setting WP_CLI_PACKAGES_DIR: /opt/wp-cli/packages in /etc/wp-cli/config.yml. Whichever user first runs wp package install creates and owns the directory, and every other user then silently loses their packages, because IncludePackageAutoloader only checks is_readable():

if ( is_readable( $autoloader_path ) ) { return [ $autoloader_path ]; }
return false;   // → handle_failure() → WP_CLI::debug( 'No package autoload found to load.' )

No error, no warning — the packages just stop existing unless you run --debug.

Why 0755 is enough

The risk #243 guards against is another local user writing into the packages directory, whose vendor/autoload.php is required on every single WP-CLI run. Read access was never part of that — these are downloaded public packages, not secrets. 0755 removes group and world write outright, and a umask can only clear further bits, so the result is never group- or world-writable:

umask old mkdir(0777) new mkdir(0755)
022 (default) 0755 0755
002 (shared group) 0775 0755
000 (common in containers) 0777 0755
077 0700 0700

The two rows in bold are the configurations that made this exploitable in the first place, and 0755 closes both.

Note this only affects directories WP-CLI creates — the is_dir() guard leaves a pre-provisioned directory untouched, so an admin who sets up a shared packages directory with a group and the setgid bit keeps full control either way.

Test change

The unit test asserted the literal string 0700, which is umask-dependent. It now asserts that the group and other write bits are unset, which is the invariant that actually matters and holds under any umask.

Testing

php -l passes on both files. I could not run PHPUnit in my environment — composer install cannot authenticate to github.com for the dev dependencies — so CI is the real check here. I did verify the permission behaviour and both assertion styles directly across the four umasks in the table above.

🤖 Generated with Claude Code

https://claude.ai/code/session_014qPfx7yCg9xCTys3XXXRkU


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved access to the default packages directory in shared-user environments.
    • Maintained protection against write access from group and other users.
  • Tests

    • Updated permission checks to accommodate system umask differences while ensuring unsafe write permissions remain disabled.

0700 breaks installations that point `WP_CLI_PACKAGES_DIR` at a location
shared between users: whichever user first runs `wp package install` owns the
directory, and every other user then silently loses their packages, because
`IncludePackageAutoloader` only checks `is_readable()` and otherwise falls
through to a debug message.

The risk being guarded against is another local user *writing* into the
packages directory, whose `vendor/autoload.php` is required on every WP-CLI
run. Read access was never part of that, so 0755 closes the hole while leaving
shared-read setups working. A umask can only clear further bits, so the result
is never group- or world-writable.

The unit test asserted a literal `0700`, which is umask-dependent. Assert that
the group and other write bits are unset instead, which is the invariant that
actually matters.

Follow-up to #243.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qPfx7yCg9xCTys3XXXRkU
Copilot AI lite review requested due to automatic review settings August 4, 2026 08:15
@swissspidy
swissspidy requested a review from a team as a code owner August 4, 2026 08:15
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@swissspidy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98a69658-34ca-4a7e-8163-0e21df4285a1

📥 Commits

Reviewing files that changed from the base of the PR and between ee60755 and 82e004a.

📒 Files selected for processing (1)
  • src/Package_Command.php
📝 Walkthrough

Walkthrough

The default Composer packages directory now uses mode 0755. The test validates that group and world write bits are unset while allowing umask-dependent permission modes.

Changes

Packages directory permissions

Layer / File(s) Summary
Directory mode and permission validation
src/Package_Command.php, tests/phpunit/ComposerJsonTest.php
The directory mode changed from 0700 to 0755. The test now checks that group and world write bits are not set, regardless of the active umask.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change from 0700 to 0755 permissions for the packages directory.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wp-cli-security-audit-t5dmuw

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.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adjusts WP-CLI package directory creation to use 0755 (instead of 0700) so installations that share WP_CLI_PACKAGES_DIR across multiple users remain readable, while still preventing group/world writes that could enable local code execution via the package autoloader.

Changes:

  • Change packages directory creation mode from 0700 to 0755 in Package_Command::create_default_composer_json().
  • Update the PHPUnit assertion to check the invariant “not group/world writable” rather than asserting a literal mode string that varies with umask.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Package_Command.php Creates the packages composer directory with 0755 to keep shared directories readable while still preventing group/world writes.
tests/phpunit/ComposerJsonTest.php Updates the permissions assertion to be umask-robust by checking write bits instead of a fixed mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +99
$perms = fileperms( dirname( $actual ) ) & 0777;
$this->assertSame(
0,
$perms & 0022,
sprintf( 'Packages directory must not be group- or world-writable, got %o.', $perms )
);
@swissspidy swissspidy added this to the 2.7.4 milestone Aug 4, 2026
@swissspidy swissspidy added the command:package Related to 'package' command label Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Package_Command.php`:
- Around line 1286-1290: Update the package setup flow around mkdir in
Package_Command to protect Composer-generated PHP files, including
vendor/autoload.php, from group or world write access. Ensure the Composer root
remains private or enforce restrictive permissions on generated PHP files after
Composer runs, while preserving support for shared WP_CLI_PACKAGES_DIR
locations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e9ede94f-0657-405e-b64e-e7bf8f93b883

📥 Commits

Reviewing files that changed from the base of the PR and between b62938b and ee60755.

📒 Files selected for processing (2)
  • src/Package_Command.php
  • tests/phpunit/ComposerJsonTest.php

Comment thread src/Package_Command.php Outdated
Comment on lines +1286 to +1290
// 0755 rather than 0700: the risk being guarded against is another local user
// *writing* into the packages directory, whose `vendor/autoload.php` is required on
// every WP-CLI run. Read access was never the problem, and revoking it breaks setups
// that point `WP_CLI_PACKAGES_DIR` at a location shared between users.
if ( ! @mkdir( $composer_dir, 0755, true ) ) { // @codingStandardsIgnoreLine

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Composer 2.10.2 src Composer Autoload AutoloadGenerator generate filePutContentsIfModified vendor/autoload.php chmod

💡 Result:

In Composer 2.10.2, the AutoloadGenerator class is responsible for generating autoloader files within the vendor directory [1][2][3]. The process involves generating several files (e.g., autoload.php, autoload_static.php, autoload_real.php) using the filePutContentsIfModified method from the Filesystem utility class [1][3][4][5]. The filePutContentsIfModified method is implemented in src/Composer/Util/Filesystem.php as follows: public function filePutContentsIfModified(string $path, string $content) { $currentContent = Silencer::call('file_get_contents', $path); if (false === $currentContent || $currentContent!== $content) { return file_put_contents($path, $content); } return 0; } This method only performs a file write operation if the target file does not exist or if the content has changed, which helps avoid unnecessary disk I/O and maintains original file timestamps when no modification is required [4]. It does not explicitly call chmod to modify file permissions during the generation of these files; standard file permissions are determined by the PHP file_put_contents function and the system's default umask settings [4].

Citations:


🌐 Web query:

Composer 2.10.2 Package_Command require composer version autoload vendor autoload.php include chmod

💡 Result:

Composer 2.10.2 is a version of the Composer dependency manager released on July 1, 2026 [1][2]. The require command is used to add new packages to your composer.json file and install them into your project [3]. When you run composer require <package-name>, Composer updates your dependencies and regenerates the autoloading files, specifically the vendor/autoload.php entry point [4][5][6]. Regarding vendor/autoload.php and chmod permissions: 1. Purpose of vendor/autoload.php: This file is the central entry point for PHP's autoloading mechanism in Composer-managed projects [4][6]. By including it at the start of your application (e.g., require DIR . '/vendor/autoload.php';), PHP will automatically load any classes from your installed packages when needed, eliminating the need for manual require or include statements for individual class files [4][7][5][6]. 2. Permission Issues and chmod: If you encounter "Permission denied" or "Failed opening required" errors when attempting to include this file, it is typically due to incorrect file system permissions or ownership [8][9][10]. - Ownership: The vendor directory and its contents should be owned by the user or service account that executes your PHP scripts (such as www-data, apache, or your own user) [8][9][10]. You can fix ownership issues using chown (e.g., sudo chown -R <user>:<group> vendor) [10]. - Permissions: Ensure the directories and files are readable by the PHP process [8]. While chmod can be used to set permissions (e.g., chmod 755 -R vendor), it is generally better practice to manage ownership correctly rather than granting broad, insecure permissions like o+w (world-writable) [11][10]. If the vendor/autoload.php file is missing, ensure you have successfully run composer install or composer update in the project root [4][6]. If you change your project's autoload configuration in composer.json, you must run composer dump-autoload to regenerate the autoloader files [4][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package composer constraints =="
git ls-files | rg '(^|/)composer\.(json|lock)$' || true
for f in $(git ls-files 'composer.*'); do
  echo "--- $f"
  sed -n '1,220p' "$f" | rg -n '"name"|"require"|"php"|"composer-plugin-api"|"composer-runtime-api"|WP_CLI' || true
done

echo "== autoload.php require paths =="
rg -n "vendor/autoload\.php|autoload\.php" src tests --glob '*.php' || true

echo "== package install/update autoloader generation hints =="
rg -n "dump-autoload|autoload|vendor/autoload|createDefaultComposer|create_default_composer|install\(|update\(" src tests/phpunit/ComposerJsonTest.php tests --glob '*.php' | head -n 200 || true

Repository: wp-cli/package-command

Length of output: 2955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== composer.json =="
cat -n composer.json

echo "== Package_Command relevant sections =="
sed -n '1,90p' src/Package_Command.php
sed -n '220,265p' src/Package_Command.php
sed -n '680,720p' src/Package_Command.php
sed -n '800,850p' src/Package_Command.php
sed -n '1240,1340p' src/Package_Command.php
sed -n '1340,1390p' src/Package_Command.php

Repository: wp-cli/package-command

Length of output: 16208


Security Misconfiguration (CWE-732): Incorrect Permission Assignment for Critical Resource

Reachability: Internal

Protect generated autoload files, not only the packages directory.

0755 clears group/other write from the directory, but Composer 2.10.2 still writes generated PHP files through file_put_contents() without clearing file write bits. On a shared packages directory with a permissive umask, a local peer with file access can overwrite vendor/autoload.php even though they cannot create files there. Enforce that Composer-created PHP files under WP-CLI packages are not group- or world-writable, or keep the Composer root private until Composer applies stricter default permissions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Package_Command.php` around lines 1286 - 1290, Update the package setup
flow around mkdir in Package_Command to protect Composer-generated PHP files,
including vendor/autoload.php, from group or world write access. Ensure the
Composer root remains private or enforce restrictive permissions on generated
PHP files after Composer runs, while preserving support for shared
WP_CLI_PACKAGES_DIR locations.

Source: MCP tools

@swissspidy
swissspidy merged commit 48cc4f5 into main Aug 4, 2026
74 checks passed
@swissspidy
swissspidy deleted the claude/wp-cli-security-audit-t5dmuw branch August 4, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

command:package Related to 'package' command

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants