Skip to content

Composer Dependency Policies (originally filter lists) #12786

@glaubinix

Description

@glaubinix

The first iteration of this feature shipped in #12766 as config.filter. It has since been replaced by a unified config.policy object that covers security advisories, malware detection, abandoned packages, and arbitrary custom dependency policies under one consistent skeleton (#12804). The sections below describe the current (post‑#12804) shape of the feature plus the follow-up work still in flight.

What it does

Composer can apply dependency policies that mark package versions as "blocked" or "reportable", and act on them in two places:

  1. At dependency-resolution time (composer update, require, remove, create-project and, for malware, composer install): matching versions are removed from the pool so they cannot be installed.
  2. At audit time (composer audit and the post-update audit summary): matching versions are surfaced and can drive the exit code.

Three dependency policies are built into Composer (advisories, malware, abandoned) and any number of additional custom dependency policies can be configured by the user or advertised by a Composer repository as filter lists.

The feature is on by default. Out of the box this means: security advisories block and fail audit, malware blocks (on both update and install) and fails audit, and abandoned packages are reported by audit but do not block.

The default malware data source on packagist.org is the Aikido malware feed, wired up via composer/packagist#1681.

Concepts

  • Dependency policy: a named set of filter entries (e.g. advisories, malware, company-policy). Every dependency policy (built-in or custom) has the same skeleton.
  • Source: where a dependency policy's data comes from. Built-in dependency policies are sourced internally (advisories via AdvisoryProviderInterface, abandoned via package metadata). Custom dependency policies get their data either from a Composer repository that advertises them or from one or more explicit URL sources.

Try it out

Hard-coded example for local experimentation only, not how you'd configure a real project:

{
    "name": "acme/project",
    "version": "1.0.0",
    "require": {
        "acme/library": "*"
    },
    "config": {
        "policy": true
    },
    "repositories": [
        {
            "type": "package",
            "package": [
                {
                    "name": "acme/library",
                    "version": "1.0.0",
                    "type": "metapackage"
                }
            ],
            "filter": {
                "malware": [
                    {
                        "package": "acme/library",
                        "constraint": "*",
                        "url": "https://example.org/malware/acme/library",
                        "reason": "This package is malware"
                    }
                ]
            }
        }
    ]
}

and run:

composer update

Configuration: the unified config.policy object

Everything that blocks or reports package versions based on external data lives under config.policy. Setting policy: false is the main kill switch: it disables advisories, malware, abandoned and every custom dependency policy.

{
    "config": {
        "policy": false
    }
}

Individual dependency policies can be turned off by setting them to false like shown below:

{
    "config": {
        "policy": {
          "abandoned": false
        }
    }
}

Every dependency policy (built-in or custom) supports the same universal skeleton:

{
    "block": bool,                    // remove matching versions from the pool?
    "audit": "ignore|report|fail",    // how `composer audit` treats matches
    "ignore": { ... },                // per-package exemptions (universal format)
}

Setting a dependency policy to false is shorthand for {"block": false, "audit": "ignore"}. Setting a dependency policy to true is shorthand for the dependency policy's defaults.

Built-in dependency policies

Name Data source Default block Default audit Extra keys
advisories Repos via AdvisoryProviderInterface true "fail" ignore-id (CVE/GHSA/PKSA), ignore-severity
malware Repos that advertise malware data via the malware filter list true "fail" block-scope ("all" (default) / "update" / "install"), ignore-source
abandoned Package metadata (abandoned field) false "fail"

advisories and abandoned only ever block during update / require / remove. malware additionally blocks during composer install by default; this is configurable via block-scope.

Reserved built-in names. Composer repositories advertising entries under filter.lists with these names are ignored:

  • advisories, abandoned

Reserved-for-future-use names (also rejected if used as custom dependency policy names or advertised by a repository): package, packages, license, licence, licenses, licences, support, maintenance, security, minimum-release-age, anything starting with ignore (the only ignore-prefixed key allowed under policy is ignore-unreachable).

Note: malware is not reserved on the repository side. It is a well-known name with built-in defaults that repositories are expected to advertise. On the user-config side, malware is reserved alongside advisories and abandoned, so it cannot be used as a custom dependency policy name.

Custom dependency policies

Any other key under policy defines a custom dependency policy. Data comes from:

  1. A Composer repository that advertises support for a filter list that can be configured as a dependency policy of that name (no sources needed), or
  2. one or more explicit sources URLs, or
  3. both (entries are merged).
{
    "config": {
        "policy": {
            "company-policy": {
                "sources": [
                    {"type": "url", "url": "https://acme.example.com/filter.json"}
                ],
                "block": true,
                "audit": "fail",
                "ignore": {
                    "vendor/internal-fork": "maintained internally"
                }
            }
        }
    }
}

Source URLs must use the https:// scheme. Dependency policy data drives blocking and audit reporting, so the transport must be authenticated and tamper-resistant. There is no secure-http-style opt-out. Non-https:// URLs are rejected both by composer validate and at config-load time.

Universal ignore format

Every dependency policy's ignore accepts package names (with * wildcards) plus optional constraints and per-rule scoping:

{
    "vendor/package": "all versions, all contexts",
    "vendor/legacy-*": {"on-audit": false, "reason": "allow install but keep blocking in audit"},
    "vendor/pinned": {"constraint": "^2.0", "reason": "only v2 still in use"},
    "vendor/multi": [
        {"constraint": "^1.0", "reason": "legacy fork, verified safe"},
        {"constraint": "^3.0", "on-block": false, "reason": "v3 flagged by mistake"}
    ]
}

on-block and on-audit default to true. Set one to false to scope the exemption.

Advisory-specific extras

  • advisories.ignore-id — keyed by CVE / GHSA / PKSA / remote ID; same value shapes as the universal ignore format.
  • advisories.ignore-severity — keyed by low, medium, high, critical.

Malware-specific extras

  • malware.block-scope"all" (default), "update", "install".
  • malware.ignore-source — array of source / repository names whose malware data should be skipped (useful when one source is too noisy).

policy.ignore-unreachable

Controls whether unreachable repositories and dependency policy sources fail the run or are silently ignored. Default: ["update", "install"]. Accepts:

  • true — ignore for all operations
  • false — never ignore (always fail)
  • array subset of ["audit", "install", "update"]

Information for Composer users

CLI flags

All present on update, require, remove, install, create-project (note that install only acts on malware, since advisories/abandoned never block on install):

  • --no-blocking — disables all dependency policy blocking (advisories, malware, abandoned, custom dependency policies) for this command. Also see COMPOSER_NO_BLOCKING.
  • --no-security-blockingdeprecated alias for --no-blocking. Old behaviour only disabled advisory blocking; the new behaviour disables everything for parity with the help text. Also see COMPOSER_NO_SECURITY_BLOCKING.

Environment variables

Env var Values Purpose
COMPOSER_POLICY 0 / 1 Main switch. 0 disables every dependency policy.
COMPOSER_NO_BLOCKING 0 / 1 Per-command equivalent of --no-blocking.
COMPOSER_NO_SECURITY_BLOCKING 0 / 1 Deprecated alias of COMPOSER_NO_BLOCKING.
COMPOSER_AUDIT_ABANDONED ignore / report / fail Override policy.abandoned.audit.
COMPOSER_POLICY_ADVISORIES_BLOCK 0 / 1 Override policy.advisories.block.
COMPOSER_POLICY_ABANDONED_BLOCK 0 / 1 Override policy.abandoned.block.
COMPOSER_POLICY_MALWARE_BLOCK 0 / 1 Override policy.malware.block.
COMPOSER_SECURITY_BLOCKING_ABANDONED 0 / 1 Deprecated alias of COMPOSER_POLICY_ABANDONED_BLOCK.
COMPOSER_NO_AUDIT 0 / 1 Skip the post-update audit summary (unchanged from before; equivalent to --no-audit).

Disabling dependency policies per repository

Per-repository overrides live under the repo-level filter key (the repo-level name stays filter because it controls behaviour for that one repository, not the global dependency policy config):

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://example.org",
            "filter": false
        }
    ]
}

Or selectively exclude dependency policies from a Composer repository:

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://example.org",
            "filter": {
                  "untrusted-list": false
            }
        }
    ]
}

Information for Composer repositories

A Composer repository advertises dependency policy support in its packages.json with the filter key:

{
    "filter": {
        "metadata": true,
        "lists": {
            "malware": { "enabled": true }
        },
        "summary-url": "/p2/filter-summary.json"
    }
}
  • metadata (bool, required): when true, per-package metadata files (served via metadata-url) may carry a filter key with entries.

  • lists (object, required): map of dependency policy names this repository provides, each mapped to an object describing the entry. The object currently carries a single enabled flag (set to true when the policy is available); Composer reads it to learn which dependency policies the repo offers, but whether a policy is active is decided entirely by the user's config.policy plus Composer's built-in defaults for well-known names. The object shape exists so future versions can attach per-policy metadata without another wire-format break.

  • summary-url (string, optional): URL (absolute or root-relative) returning a compact mapping of dependency policy name → package name → version constraint. When configured, Composer fetches it during install, update and audit and uses it to skip per-package metadata fetches for packages that cannot match any active dependency policy. Revalidated on each run via If-Modified-Since using the same on-disk repository cache as package metadata. Response shape:

    {
        "filter": {
            "malware": {
                "vendor/package": ">=1.0.0,<1.2.0",
                "other/package": "*"
            }
        }
    }
  • api-url (string, optional): URL (absolute or root-relative) that accepts a POST body with the relevant package PURLs and configured dependency policy names and returns the matching filter entries directly. Intended for cases where the summary would be too large to serve in full. When set, Composer uses api-url instead of summary-url and per-package metadata for filter purposes; if both summary-url and api-url are advertised, api-url takes precedence and summary-url is ignored. The POST is not cached client-side because each request body differs. Request and response shapes:

    {
        "packages": ["pkg://composer/vendor/package", "pkg://composer/other/package"],
        "lists": ["malware"]
    }
    {
        "filter": {
            "malware": [
                {
                    "package": "vendor/package",
                    "constraint": ">=1.0.0,<1.2.0",
                    "url": "https://example.org/filters/123",
                    "reason": "Malware",
                    "id": "PKFE-xxxx-xxxx-xxxx"
                }
            ]
        }
    }

    Each entry has the same shape as a per-package metadata filter entry, with the addition of the package field (since one response covers many packages).

Added in #12833 (summary-url) and #12839 (api-url).

Per-package metadata files include matching entries under a top-level filter key:

{
    "packages": { "vendor/package": [ ... ] },
    "filter": {
        "malware": [
            {
                "constraint": ">=1.0.0,<1.2.0",
                "url": "https://example.org/filters/123",
                "reason": "Malware",
                "id": "PKFE-xxxx-xxxx-xxxx",
                "source": "aikido"
            }
        ]
    }
}

Repositories must not advertise dependency policies with reserved names (see "Built-in dependency policies" above). Composer drops those entries.

Information for third-party source providers

Custom dependency policies can be backed by an HTTP endpoint. Configure them under the dependency policy's sources:

{
    "config": {
        "policy": {
            "company-policy": {
                "sources": [
                    {"type": "url", "url": "https://example.org/php/filters"}
                ]
            }
        }
    }
}

Source URLs must use the https:// scheme (see "Custom dependency policies" above).

Sources can be configured via Composer CLI:

composer policy add-source [policy-name] url [url]

Composer issues a POST to the configured URL with a JSON body listing PURLs and the names of the configured dependency policies this URL is backing:

{
    "packages": ["pkg://composer/acme/package", "pkg://composer/acme/library"],
    "lists": ["company-policy"]
}

The endpoint responds with the matching filter entries:

{
    "filter": [
        {
            "package": "acme/package",
            "constraint": "1.0 || 1.1 || 1.2",
            "url": "https://example.org/packages/acme/package/filters",
            "reason": "...",
            "id": "optional identifier"
        }
    ]
}

Backward compatibility / migration

config.audit.* keys

All released audit.* keys continue to work as a fallback for users who have not migrated to config.policy. The fallback is all-or-nothing per built-in dependency policy, not per individual key:

  • If config.policy.advisories is set to any value (including the literal false), every audit.* key that maps to advisories (block-insecure, ignore, ignore-severity) is ignored entirely. Only policy.advisories.* is read. Mix-and-matching, e.g. setting policy.advisories.block while expecting audit.ignore-severity to still apply, is not supported. Migrate all advisories-related settings together.
  • If config.policy.abandoned is set to any value (including the literal false), every audit.* key that maps to abandoned (block-abandoned, abandoned, ignore-abandoned) is ignored entirely. Only policy.abandoned.* is read.
  • The two built-in dependency policies are independent: configuring policy.advisories while leaving the abandoned settings under audit.* is allowed, and vice versa.
  • audit.ignore-unreachable is superseded by policy.ignore-unreachable as soon as the latter is set.

Migration functionality (including a deprecation warning whenever any audit.* key is read) will likely ship with Composer 2.11.

Old (config.audit.*) New (config.policy.*)
block-insecure advisories.block
block-abandoned abandoned.block
abandoned abandoned.audit
ignore (CVE/GHSA/…) advisories.ignore-id
ignore (package name) advisories.ignore
ignore-severity advisories.ignore-severity
ignore-abandoned abandoned.ignore
ignore-unreachable policy.ignore-unreachable

Note that the new ignore-shape uses on-block / on-audit booleans where the legacy audit.ignore accepted an apply: "audit"|"block"|"all" field.

Documentation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for Feature.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions