Catching vulnerabilities before they become incidents
Unreviewed Composer dependencies are among the most common entry points in Magento and PHP projects. This article shows how composer audit, Roave Security Advisories and automated CI pipelines reliably surface known vulnerabilities before they reach production, and how teams separate real risk from pure scan noise without manually reviewing every single warning.
Table of Contents
- 1. Why dependency vulnerabilities matter in Magento and PHP projects
- 2. composer audit: the built-in command, usage and output
- 3. Interpreting severity correctly: CVSS, exploitability, reachability
- 4. Roave Security Advisories: preventing vulnerable installs before they happen
- 5. CI integration with GitHub Actions
- 6. CI integration with GitLab CI and an allowlist for accepted risk
- 7. Unfixable transitive dependencies and false positives
- 8. Triage workflow and documenting risk acceptance
- 9. Complementary tools and a comparison of scanning approaches
- 10. Summary
- 11. FAQ
1. Why dependency vulnerabilities matter in Magento and PHP projects
A typical Magento 2 project directly requires maybe 40 to 60 packages through composer.json. Once you count every transitive dependency in the actually installed tree, the vendor directory frequently holds 300 to 500 Composer packages. Every single one of them is a potential attack vector, regardless of whether your own code ever interacts with it directly. Known vulnerabilities in widely used libraries such as guzzlehttp/guzzle, symfony/http-foundation or monolog/monolog get swept up by automated scanners across the internet within hours of a CVE being published. No targeted attack against a specific shop is required for this, it is enough that a vulnerable version number is visible through an HTTP header or a publicly reachable composer.lock.
Magento adds its own twist: many third party extensions ship their own Composer dependencies with tight version constraints that block a composer update for months, because an update would otherwise trigger version conflicts elsewhere. That exact situation is where outdated, vulnerable packages sit unnoticed in a project, often for years, because nobody regularly checks whether an already installed version has meanwhile been flagged as unsafe. Dependency scanning closes exactly that gap: it does not inspect your own code, it systematically checks whether any of the referenced versions is listed as vulnerable in a public advisory database, making a risk visible that would otherwise stay completely hidden.
2. composer audit: the built-in command, usage and output
Since Composer 2.4, composer audit has been a built-in command that ships with every standard installation, no additional plugin required. The command compares the versions pinned in composer.lock against the FriendsOfPHP Security Advisories database, a curated collection of known PHP package vulnerabilities that Packagist itself also uses as the data source for a package's security status. A plain composer audit is enough for an immediate, human readable overview with package name, affected version range, severity and a link to the relevant advisory.
For CI integration, composer audit --format=json --locked is the relevant variant: --locked reads only composer.lock without actually installing the vendor folder, which speeds up the scan noticeably and also works in lean build containers without a full dependency install. The JSON result exposes two top level keys: advisories with the actual vulnerability findings per package, and abandoned with packages flagged as no longer maintained, which is a subtler risk of its own. composer audit returns a non-zero exit code whenever advisories are found, which lets you use the command directly as a CI gate without any additional tooling.
# Run composer audit against the locked dependency tree
composer audit
# Machine-readable output for tooling and CI pipelines
composer audit --format=json --locked
# Example JSON output (abbreviated) for a vulnerable guzzlehttp/psr7 version
{
"advisories": {
"guzzlehttp/psr7": [
{
"advisoryId": "PKSA-h4kf-cchx-w9wf",
"packageName": "guzzlehttp/psr7",
"affectedVersions": "<1.9.1|>=2,<2.4.5",
"title": "Bypass of file:// URI validation in MimeType detection",
"cve": "CVE-2023-29197",
"link": "https://github.com/advisories/GHSA-wxmh-65f7-jcvw",
"severity": "medium"
}
]
},
"abandoned": {
"swiftmailer/swiftmailer": null
}
}
# Exit code 1 signals at least one open advisory, useful for CI gating
echo "Exit code: $?"
One important detail for Magento projects: composer audit checks require-dev packages by default too. For pure production deployments, where PHPUnit, Symfony VarDumper or other dev tools are never shipped anyway, --no-dev can noticeably reduce the number of reported advisories and keep the focus on packages that are actually relevant in production. Still, the daily CI pipeline should keep a separate run without --no-dev, because even a compromised dev dependency can become a risk during the build process itself, for example through a malicious post-install hook.
3. Interpreting severity correctly: CVSS, exploitability, reachability
Every advisory in the composer audit output carries a severity, usually low, medium, high or critical, derived from the Common Vulnerability Scoring System (CVSS). CVSS rates a vulnerability along several axes: attack vector (local versus reachable over the network), attack complexity, required privileges and the impact on confidentiality, integrity and availability. A high CVSS score means the vulnerability is theoretically severe, but it says nothing about whether your own code ever actually calls the vulnerable code path.
This is exactly where the concept of reachability comes in: a critical deserialization flaw in an XML library is often practically irrelevant for a project that only ever writes with that library and never parses untrusted input with it. A realistic assessment therefore always requires a quick look at the affected code: is the vulnerable function called with user input, or only internally with static, trusted data? Exploitability adds a further question, whether publicly known exploit code exists or active exploitation in the wild has been observed, information that is usually linked from the advisory itself or from references such as the GitHub Security Advisory Database.
The rule of thumb for Magento teams: critical and high advisories with a network attack vector and a known exploit are always prioritized and fixed promptly, regardless of the reachability assessment, because the risk of misjudging them is too high. For medium and low advisories, the extra reachability check pays off, letting effort go exactly where it actually reduces risk instead of treating every finding with the same priority.
4. Roave Security Advisories: preventing vulnerable installs before they happen
While composer audit checks after the fact whether already installed packages are vulnerable, roave/security-advisories takes a preventive approach. The package itself contains no code at all, only a composer.json with explicit conflict entries for every known vulnerable version range across hundreds of PHP packages. Once roave/security-advisories is required as a require-dev dependency, Composer automatically refuses to install or update to any version flagged as vulnerable in that conflict list, before the code ever lands in the project.
{
"require-dev": {
"roave/security-advisories": "dev-latest",
"phpunit/phpunit": "^10.5"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true
},
"scripts": {
"post-update-cmd": [
"@php bin/magento setup:di:compile"
]
}
}
The decisive difference from composer audit: Roave prevents the problem proactively at the moment of composer require or composer update, while composer audit only surfaces it afterwards, once the vulnerable version already sits in composer.lock. Both approaches complement each other but do not replace one another: Roave does not retroactively protect against already installed legacy packages, and composer audit does not intervene before an update actually runs. The dev-latest variant of Roave is continuously updated, so new advisories flow automatically into the conflict list as soon as they are published, with no manual action required in your own project.
In practice a drawback shows up: Roave can trigger resolution conflicts with very tight version constraints from other packages when a project has good reasons to stay on a version flagged as vulnerable, for example because a fix is not compatible with the PHP or Magento version in use. Composer offers a way out for this case, overriding individual conflict entries with a dedicated replace directive in the project's own composer.json, which should be documented and tied to a conscious risk decision, never done silently.
5. CI integration with GitHub Actions
A composer audit that only runs occasionally by hand on a developer's own machine loses its value the moment a developer forgets the step or skips it under time pressure. The reliable solution is an automated CI job that runs on every pull request and additionally on a fixed schedule. The schedule matters because new advisories can be published at any time, even for code that has not changed in weeks: an already merged state can become vulnerable retroactively, without any new commit triggering it.
name: Dependency Audit
on:
pull_request:
schedule:
- cron: "0 6 * * 1" # weekly Monday scan, catches new advisories on unchanged code
jobs:
composer-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
tools: composer:v2
- name: Install dependencies (locked, no scripts)
run: composer install --no-scripts --prefer-dist
- name: Run composer audit
run: composer audit --locked --format=json > audit-report.json
continue-on-error: true
- name: Evaluate audit report against severity threshold
run: bash ./bin/ci-audit-gate.sh audit-report.json high
- name: Upload audit report
if: always()
uses: actions/upload-artifact@v4
with:
name: composer-audit-report
path: audit-report.json
The --no-scripts flag matters for Magento projects, because composer install would otherwise try to run project specific post-install hooks such as Magento module compilation, which costs unnecessary runtime in a lean audit job and would require extra prerequisites like a working database connection that a pure dependency scan does not need at all. Uploading the JSON report as an artifact ensures that even a failing audit job leaves the concrete advisory details traceable in the GitHub interface, instead of showing just a red status with no context.
6. CI integration with GitLab CI and an allowlist for accepted risk
In GitLab CI, the composer_audit job follows the same basic principle, but often adds a project specific allowlist for advisories that have been deliberately classified as an acceptable risk. Without this mechanism, every unfixable or knowingly accepted advisory leaves the pipeline permanently red, which quickly tempts teams into disabling the whole audit step instead of refining it, a classic alert fatigue problem.
composer_audit:
stage: test
image: php:8.4-cli
before_script:
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-scripts --prefer-dist
script:
- composer audit --locked --format=json > audit-report.json || true
- |
# Filter out advisories explicitly accepted in security/allowlist.json
jq --slurpfile allow security/allowlist.json '
.advisories
| to_entries
| map(.value |= map(select(.advisoryId as $id | ($allow[0].acceptedAdvisories | index($id)) | not)))
| map(select(.value | length > 0))
' audit-report.json > unresolved-advisories.json
- |
if [ "$(jq "length" unresolved-advisories.json)" -gt 0 ]; then
echo "Unresolved advisories found, failing pipeline:"
cat unresolved-advisories.json
exit 1
fi
artifacts:
when: always
paths:
- audit-report.json
- unresolved-advisories.json
The allowlist itself lives as a version controlled JSON file in the repository, for example under security/allowlist.json, with the fields acceptedAdvisories for the advisory IDs, reason for the business justification and reviewDate for the date of the last check. This approach makes risk acceptance traceable and version controlled, instead of leaving it as a silent exception in a single developer's head. A regular review slot, for example quarterly, checks whether an accepted advisory has since received a fix and the allowlist can be cleaned up accordingly.
7. Unfixable transitive dependencies and false positives
Not every reported advisory can be fixed with a simple composer update. A vulnerable version can sit deep inside the transitive dependency chain of a third party module whose maintainer has not yet published a compatible fix. In that case, composer why-not packagename version helps make the exact chain visible, showing which direct package forces which vulnerable transitive dependency, and composer prohibits shows which version would actually be needed to resolve the conflict.
False positives arise in practice mainly through three patterns: advisories for code paths that are never reached in your own project, advisories for require-dev-only packages that never ship to production, and stale advisory metadata, where a fix has long existed but the database has not yet marked the new, safe version as such. Every one of these cases justifies a documented exception through the allowlist, but none of them justifies disabling the scan entirely, because the assessment can change again with every advisory update.
A pragmatic intermediate step for unfixable packages: check whether an alternative library with comparable functionality exists that can replace the vulnerable package without waiting for a full module update. composer.json also allows targeted replace entries to swap a package for your own patched fork version, a stopgap that should be documented and rolled back again once the next official fix ships.
8. Triage workflow and documenting risk acceptance
A repeatable triage workflow keeps dependency scanning from turning into an ignored checkbox exercise. The first step after every scan result is classification: immediately fixable, fixable with effort, or currently unfixable. Immediately fixable advisories, usually a simple composer update packagename, get resolved directly in the same pull request. Advisories that require more effort, for example a major version update with breaking changes, get filed as a dedicated ticket with a deadline depending on severity.
#!/usr/bin/env bash
# ci-audit-gate.sh - fail CI only when advisories reach a given severity
set -euo pipefail
REPORT_FILE="${1:?Usage: ci-audit-gate.sh <report.json> <min-severity>}"
MIN_SEVERITY="${2:-high}"
declare -A SEVERITY_RANK=( [low]=1 [medium]=2 [high]=3 [critical]=4 )
threshold="${SEVERITY_RANK[$MIN_SEVERITY]}"
# Extract every advisory severity from the composer audit JSON report
mapfile -t severities < <(jq -r '.advisories[][] | .severity // "medium"' "$REPORT_FILE")
blocking=0
for sev in "${severities[@]}"; do
rank="${SEVERITY_RANK[$sev]:-2}"
if (( rank >= threshold )); then
blocking=$((blocking + 1))
fi
done
total="${#severities[@]}"
echo "Found $total advisories, $blocking at or above severity '$MIN_SEVERITY'"
if (( blocking > 0 )); then
echo "Blocking advisories detected, failing pipeline" >&2
exit 1
fi
echo "No advisories at or above threshold, pipeline continues"
exit 0
For unfixable advisories, documenting risk acceptance is the decisive step that gets skipped most often in practice. A complete risk acceptance note contains at least four things: the advisory ID, the concrete justification for why the risk is considered acceptable in the project context, the name of the responsible person and a follow up review date. Without this documentation, the knowledge behind a conscious decision disappears within a few months, and a new team member either repeats the same analysis or misses the risk entirely because it was never recorded anywhere visible.
The threshold script from the code example above deliberately separates report generation from evaluation: composer audit itself has no concept of exceptions or thresholds, it reports every found advisory equally. The evaluation logic therefore belongs in a separate, version controlled script that lives alongside the allowlist in the repository and goes through the same code review process on every change as any other production code.
9. Complementary tools and a comparison of scanning approaches
Beyond composer audit and Roave Security Advisories, a few complementary tools are worth a look. The original standalone Symfony CLI Security Checker was discontinued in 2022, but its functionality lives on in the Symfony CLI through symfony security:check, which uses the same FriendsOfPHP advisory database as composer audit but returns a partly different output format for existing tooling integrations. GitHub Dependabot works quite differently: it does not just scan composer.lock, it automatically opens pull requests with suggested version updates as soon as an advisory is published for a package in use, including a changelog reference and a compatibility score.
Dependabot and composer audit complement each other well: Dependabot automates the update suggestions, while composer audit in the CI pipeline independently verifies that no vulnerable version actually remains in composer.lock, even if a Dependabot pull request has not been merged for whatever reason. For Magento specific packages beyond the plain PHP ecosystem, keeping an eye on the Adobe Security Bulletin feed also remains relevant, because Magento core patches are sometimes published outside the regular Composer advisory flow and use their own version numbering scheme.
| Criterion | Manual Review (No Scanning) | composer audit in CI | Roave Security Advisories + Dependabot |
|---|---|---|---|
| Detection speed | Weeks to months, dependent on chance discovery | Within minutes of every build | Immediately on composer update, PR within hours |
| Point of protection | Purely reactive, only after an incident | Reactive, but automated and consistent | Preventive, blocks the install directly |
| Handling false positives | No system, every check starts from scratch | Allowlist with documented justification | replace overrides requiring review |
| Transitive dependency coverage | Practically infeasible by hand | Complete via composer.lock | Complete via conflict rules |
| Maintenance effort | High, unreliable, depends on individuals | Low after setup, runs automatically | Low, Dependabot PRs need review |
In practice, none of these approaches is sufficient in isolation. composer audit in the CI pipeline keeps vulnerable code from silently going live. Roave Security Advisories keeps it from being installed in the first place. Dependabot automates the update suggestions, and a documented triage process with an allowlist keeps the team from drowning in scan noise, letting it focus on real, prioritized risk instead.
Mironsoft
Security audits, CI/CD pipelines and Magento hardening for Composer based projects
Want dependency scanning reliably anchored in your CI pipeline?
We set up composer audit, Roave Security Advisories and a documented triage process in your GitHub Actions or GitLab CI pipeline, including an allowlist concept and threshold gates that do not flood your team with scan noise.
CI integration
composer audit as a pull request and scheduled gate in GitHub Actions or GitLab CI
Roave setup
Preventive install blocking for known vulnerabilities directly in composer.json
Triage process
Allowlist, thresholds and documented risk acceptance for unfixable advisories
10. Summary
Reliable dependency scanning for Magento and PHP projects always combines several layers: composer audit surfaces already installed, vulnerable versions in composer.lock and, thanks to its built-in JSON format and meaningful exit code, drops into any CI pipeline without extra tooling. Roave Security Advisories adds a preventive layer that never lets a vulnerable version into the project in the first place. CVSS derived severity gives a first pass at prioritization, but without a reachability assessment it is only half the story, because a high severity says nothing about actual exploitability along your own code path.
The decisive success factor is not the scan itself, but how its results are handled. A documented allowlist with justification and a follow up date keeps unfixable transitive dependencies or clear false positives from becoming a permanent construction site. A threshold gate that only actually blocks above a certain severity keeps the CI pipeline green for real, prioritized risk instead of sounding the alarm on every single new advisory. Combining composer audit, Roave Security Advisories, Dependabot and a documented triage process meaningfully reduces the risk of unnoticed vulnerabilities without overwhelming the team with scan noise.
Dependency Scanning with Composer Audit - The Key Takeaways
composer audit
Built-in Composer command since 2.4, --format=json --locked for fast, CI-ready scans without a full install.
Reading severity correctly
CVSS score plus a reachability check decide the real priority, not the severity label alone.
Roave Security Advisories
Blocks the install of known vulnerable versions right at composer require, adding a preventive layer to composer audit.
Triage over noise
An allowlist with justification and a follow up date keeps teams from disabling the scan entirely over false positives.