Every PHP application built with Composer runs code from hundreds of maintainers it has never met, and in 2026 attackers showed they can change what an existing, trusted version installs by rewriting Git tags. Composer and Packagist.org have added strong controls in response: plugin allowlisting, a unified dependency policy that blocks malware and vulnerable versions, a cooldown period for new releases, and repository priorities that defeat dependency confusion. Most of them only help if you configure them deliberately. This guide covers how Composer resolves and installs code, where the trust boundaries are, the exact configuration that hardens a project, how to run private repositories and credentials safely, and how to detect and respond to a compromised package. It is written for PHP developers, platform engineers and application security teams, and reflects Composer 2.11 (current at 11 November 2026).
Key takeaways
- The lock file is your security boundary. Commit
composer.lock, install from it in CI and production, and review every change to it like code. - Plugins run code; dependency scripts do not. Composer only executes scripts defined by the root package, but plugins execute during every run, so keep
allow-pluginsexplicit and short. - Composer 2.10 and later can block bad versions. The
config.policyblock covers security advisories, malware and abandoned packages, and Composer 2.11 adds acooldownperiod for new releases. - Repository order matters. All repositories are canonical by default in Composer 2, which stops a public package from overriding your private one; do not turn that off.
- Tokens are the real target. GitHub tokens, Packagist credentials and
auth.jsonfiles are what attackers use to publish malicious versions and what stealers harvest from developer machines.
How Composer installs code and where trust is placed
Composer reads composer.json, resolves version constraints against the configured repositories (Packagist.org is added implicitly as the last one unless disabled), writes the exact resolved versions to composer.lock, and downloads each package as a dist archive or a source checkout. By default since Composer 2.1 it installs from dist. Packagist.org itself mostly stores metadata: package versions map to tags and commits in the maintainer's VCS repository, so whoever controls the repository and its tags controls what users receive.
| Stage | Trust assumption | How it is attacked | Main control |
|---|---|---|---|
| Maintainer account (Packagist, GitHub) | Only the maintainer can publish or repoint a package | Credential stuffing, stolen tokens | MFA, short-lived and narrowly scoped tokens |
| VCS tags | A released version does not change | Tags rewritten to point at malicious commits | Lock file with commit references and hashes; Packagist version immutability; cooldown |
| Package resolution | The name resolves to the intended source | Dependency confusion, typosquatting | Canonical repositories, only filters, intake review |
| Install time | Installing does not execute untrusted code | Malicious plugins | allow-plugins, --no-plugins where possible |
| Runtime | Vendor code is what was reviewed | Autoloaded files executing on every request | Lock review, malware policy, runtime monitoring |
The Packagist.org team's May 2026 security update describes two such incidents that year, intercom/intercom-php (30 April 2026) and laravel-lang (22 May 2026), and notes that attackers often modified existing Git tags after the fact. In the Laravel-Lang Composer compromise, a stolen GitHub token with organisation-wide push access was used to repoint release tags to commits in a fork; a new file ran whenever Composer's autoloader loaded and fetched a credential stealer. An earlier incident in May 2023 showed the account layer: Packagist reported that an attacker used passwords leaked from other services to access four inactive maintainer accounts with access to 14 packages and pointed them at forks (no malicious code was distributed in that case).
Lock files and reproducible installs
- Commit the lock file for applications:
composer installinstalls exactly what is locked;composer updatere-resolves. CI and production should only ever runinstall. - Review lock diffs: Packagist's advice after the 2023 takeover was to review lock file changes for untrusted dependencies or external URLs. Look for new packages, changed
sourceordistURLs, unexpected references, and versions published in the last few days. - Use Composer's integrity checks:
composer validateconfirms the lock file matchescomposer.json, and Composer 2.11 makesinstallfail when the lock file has a content-hash merge conflict. - Treat the lock as input, not just output: Composer 2.11.2 (released 11 November 2026) fixed several issues that a crafted
composer.lockorvendor/composer/installed.jsoncould trigger, including arbitrary command execution and credentials being sent to other hosts. Upgrade Composer itself, and do not accept lock file changes from untrusted pull requests without review. - Keep production lean: install with
--no-devand an optimised or authoritative classmap (--classmap-authoritative), and never placevendor/under the web root. CISA's KEV catalogue includes CVE-2017-9841, a PHPUnit command injection that is exploitable when development dependencies are reachable over HTTP.
Plugins and scripts
Composer's documentation states that only scripts defined in the root package's composer.json are executed; scripts declared by dependencies are ignored. That is a meaningful difference from ecosystems where every dependency can run install hooks. Plugins are the exception: a package of type composer-plugin runs code inside Composer during install and update.
- allow-plugins: since Composer 2.2.0 this setting controls which plugins may execute. It defaults to
{}, which allows none; entries are package name patterns set totrue(allow) orfalse(disallow and stop prompting). Avoid wildcards such as"*": true, and prefer exact names over vendor patterns for third parties. - Non-interactive runs: in CI Composer fails rather than prompting when an unlisted plugin appears, which surfaces a new plugin as a build failure instead of silently running it.
- Disable when not needed:
--no-pluginsand--no-scriptsare available for jobs that only need files on disk, such as dependency analysis or SBOM generation. - Autoload files: packages can declare
autoload.filesentries that are included whenever the autoloader loads. This is how the Laravel-Lang payload ran, and it is why a reviewed lock file matters more than install-time controls alone.
Dependency policy: advisories, malware, abandoned packages and cooldown
Composer 2.10.0 (28 May 2026) introduced the config.policy block, replacing most of the older config.audit settings, which still work as a fallback. Composer 2.11.0 (29 October 2026) added cooldown. The defaults below are from Composer's configuration documentation.
| Policy | Default behaviour | Key options | Recommended setting |
|---|---|---|---|
advisories | block: true (versions with active advisories cannot be installed during update, require or remove); audit: fail | ignore-id with reasons and on-block/on-audit scoping; ignore-severity | Keep defaults; record every ignore with a reason and review date |
malware | block: true; block-scope: all (update and install); audit: fail | ignore, ignore-source | Keep defaults so a version flagged after locking fails the next install |
abandoned | block: false; audit: fail | ignore with reasons | Report in CI; set block to true for new projects |
cooldown (2.11+) | period: null (off) | period such as "7 days"; ignore for your own vendor namespace | Set a period (NHI Mgmt Group suggests 3 to 7 days) and exempt internal packages |
The cooldown is measured from the publication time reported by the repository (published-time, which Packagist.org provides), which package authors cannot influence. It never withholds dev versions, locked packages that are not being updated, or platform packages. Packagist.org imports malware detection results from Aikido (since March 2026), published as a filter list under CC-BY 4.0 and enabled in Composer without configuration.
Bypass switches: --no-blocking (or COMPOSER_NO_BLOCKING) disables all policy blocking for a command, and "policy": false disables it entirely. Search pipelines and Dockerfiles for both; they should only appear in a documented emergency.
composer audit: reports advisories, malware, abandoned packages and custom policy matches, returning exit code 0 for no issues and 1 for findings. Run composer audit --locked in CI so the check covers what will be deployed, and use --format=json to feed findings into your vulnerability tooling. See the Software Composition Analysis (SCA) Guide.
Private repositories, priorities and dependency confusion
Composer's repository priorities article explains the risk directly: if a private repository were not canonical and someone published foo/bar 2.999 to Packagist.org, Composer would pick the higher public version. In Composer 2 all repositories are canonical by default, so a package found in a higher-priority repository is never loaded from a lower one.
- Never set
"canonical": falseon a private repository unless you understand the override risk. - Filter repositories: use
onlyorexcludeso each repository serves only the vendor namespaces you expect. - Register your vendor prefix publicly: claiming your vendor name on Packagist.org prevents others publishing under it.
- Mirror and proxy: a private repository (Satis, a hosted private Packagist service or an artefact manager) gives you a cached copy of every version you depend on, an approval point, and protection when an upstream tag is rewritten. Disable Packagist.org entirely in projects that should only resolve through the proxy. See the Private Package Repository and Proxy Guide and the Malicious Package and Dependency Confusion Guide.
- Transport:
secure-httpdefaults to true and restricts downloads to HTTPS;disable-tlsis documented by Composer as a security risk. Neither should be changed.
Where identity fits: tokens and auth.json
Composer credentials are non-human identities with direct supply chain impact. Composer can read them from a project auth.json, a global auth.json, composer.json itself, or the COMPOSER_AUTH environment variable.
- Project auth.json: Composer's documentation says to make sure it is in
.gitignore. Add it to secret scanning rules as well. - COMPOSER_AUTH: preferred in CI from a secrets manager, but Composer warns that values set on the command line may persist in shell history files.
- Read-only, scoped tokens: tokens used to install private packages need read access to specific repositories only. GitHub tokens with organisation-wide write access are what made the Laravel-Lang attack possible. See the GitHub Personal Access Tokens Security Guide.
- Maintainer accounts: enable MFA on Packagist.org and on the VCS host. Packagist's May 2026 update states MFA is available but not yet mandatory, with mandatory MFA planned.
- Developer machines: stealers delivered through packages harvest
auth.json, Git credentials and cloud keys. After any malicious package install, rotate every credential reachable from that machine or runner.
Implementation roadmap
- Upgrade Composer: standardise on the current 2.11.x release in developer images and CI, so policy blocking, cooldown and the latest lock file fixes apply.
- Baseline: run
composer audit --locked --format=jsonacross all repositories; record findings, abandoned packages and the plugins each project allows. - Tighten configuration: commit explicit
allow-plugins, keep policy defaults, set a cooldown with internal namespaces exempted, and addonlyfilters to private repositories. - Enforce in CI: fail builds on
composer auditfindings and on lock file drift; flag new plugins and new direct dependencies for review. Start in report mode ("audit": "report") for one sprint if the backlog is large, then switch to fail. - Introduce a proxy: route installs through a private repository, then disable Packagist.org in production builds.
- Fix credentials: move tokens into a secrets manager, scope them read-only, set expiry, and enable MFA for all maintainers of your published packages.
- Rehearse response: run a tabletop where a direct dependency's tags are rewritten. Rollback is reinstalling from a known-good lock file and cached dist archives.
Decision guidance
- Cooldown length: longer periods catch more malicious releases before you install them but delay security fixes. Use
ignoreor a targeted update for urgent patches rather than shortening the period for everything. - Proxy or direct: a small team with few dependencies can rely on lock review, policy defaults and cooldown. Larger organisations, and anyone publishing packages others depend on, benefit from a proxy that caches versions and centralises policy.
- Libraries versus applications: libraries usually do not ship their lock file to consumers; their security depends on maintainer account protection, signed tags and narrow tokens. Applications should always commit and enforce the lock.
- Automated updates: dependency bots are valuable, but auto-merging lock changes removes the review step attackers rely on you skipping. See the Dependency Update Automation Guide.
Detection and measurement
- Lock file changes: alert on new packages, changed source URLs or references for an unchanged version number, and versions younger than your cooldown.
- Policy bypasses: any use of
--no-blocking,COMPOSER_NO_BLOCKINGor"policy": falsein pipelines. - Runtime behaviour: PHP processes making outbound connections to unknown hosts or reading credential files after a deployment.
- Packagist transparency log: watch ownership, maintainer and version reference changes for your critical dependencies.
Metrics: percentage of repositories with an explicit allow-plugins list and no wildcards; percentage running composer audit --locked as a blocking CI step; mean time to remediate high and critical advisories; number of active advisory ignores past their review date; percentage of Composer tokens that are read-only and expiring; percentage of builds resolving only through the internal proxy.
Common failure patterns
- Running
composer updatein CI or production: silently pulls whatever is newest, including a version published minutes ago. - Wildcard plugin approval:
"allow-plugins": trueor broad vendor patterns approve any future plugin. - Exposed vendor directory: development tools such as PHPUnit reachable over HTTP, as in CVE-2017-9841.
- Committed auth.json or tokens in Dockerfiles: private repository tokens end up in Git history and image layers. See the PHP and WordPress Security Guide for the wider PHP hardening picture.
- Permanent ignores: advisories ignored to unblock a release and never revisited.
- Over-scoped maintainer tokens: one token with write access to every repository in an organisation, as in the Laravel-Lang case.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | SR-3 Supply Chain Controls and Processes; SR-4 Provenance; SR-11 Component Authenticity; SI-7 Software, Firmware, and Information Integrity; RA-5 Vulnerability Monitoring and Scanning; IA-5 Authenticator Management; CM-7 Least Functionality | Repository and intake controls; lock file references; malware blocking; audit; token management; plugin restriction |
| NIST CSF 2.0 | GV.SC-05; GV.SC-07; ID.AM-02; ID.RA-01; PR.PS-02 | Supply chain requirements and monitoring; dependency inventory; advisories; keeping packages maintained |
| CIS Controls v8.1 | Control 2 (Software Assets), 7 (Continuous Vulnerability Management), 16 (Application Software Security) | Dependency inventory, audit and remediation, secure build practice |
| ISO/IEC 27001:2022 Annex A | A.5.21 Managing information security in the ICT supply chain; A.8.8 Management of technical vulnerabilities; A.8.28 Secure coding | Third-party code governance, advisory handling, dependency practice |
| MITRE ATT&CK | T1195.001 Compromise Software Dependencies and Development Tools; T1552.001 Credentials In Files | Malicious package versions; theft of auth.json and other credential files |
| MITRE CAPEC | CAPEC-538 Open-Source Library Manipulation | Tag rewriting and malicious releases |
Practitioner checklist
NHI Mgmt Group recommends the following checks.
- Run Composer 2.11.x everywhere, including CI images and developer containers.
- Commit
composer.lock, run onlycomposer installin CI and production, and review lock diffs. - Keep an explicit, minimal
allow-pluginslist with no wildcards. - Keep advisory and malware blocking on, set a cooldown period, and document every ignore with a reason.
- Run
composer audit --lockedas a blocking CI step and send JSON output to your vulnerability tracker. - Keep private repositories canonical, filter them with
only, and route installs through a proxy. - Store Composer credentials in a secrets manager, scope them read-only, and keep
auth.jsonout of Git and images. - Deploy with
--no-devand keepvendor/outside the web root. - Enable MFA for every maintainer of packages you publish.
Standards and references
- Composer: Config (allow-plugins, policy, secure-http)
- Composer: Command-line interface (install, audit, environment variables)
- Composer: Repository priorities
- Composer: Authentication for privately hosted packages
- Packagist: An update on Composer and Packagist supply chain security
- Packagist: Composer 2.10 release
- Packagist: Packagist.org maintainer account takeover
- CISA: Known Exploited Vulnerabilities Catalog
Related NHI Mgmt Group resources: Malicious Package and Dependency Confusion Guide · Package Registry Security Comparison Guide · Private Package Repository and Proxy Guide · Open source package compromise