GHSA-x8g9-h984-pc36 is a medium-severity (CVSS 6.5) Server-Side Request Forgery (SSRF) vulnerability in pontedilana/php-weasyprint. A fix is available for pontedilana/php-weasyprint — see the affected versions and patch details below.
PhpWeasyPrint vulnerable to SSRF and local file disclosure via the attachment option
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-x8g9-h984-pc36.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-x8g9-h984-pc36 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 378,567 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
pontedilana/php-weasyprintReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
pontedilana/php-weasyprint fetches the content of option values server-side via file_get_contents() when the value looks like a URL, without restricting the URL scheme. The attachment option of Pdf is the reachable sink: any value that passes isOptionUrl() (filter_var(..., FILTER_VALIDATE_URL)) is downloaded by the PHP process and embedded into the generated PDF. Because FILTER_VALIDATE_URL accepts http, https, ftp, file and PHP stream wrappers such as php://, an attacker who can influence the attachment value reaches both a Server-Side Request Forgery primitive (e.g. internal HTTP endpoints, cloud metadata) and a local file disclosure primitive (file://, php://filter/...), with the fetched bytes exfiltrated as a PDF attachment.
This is the same class of issue KnpLabs/snappy patched for its xsl-style-sheet option in GHSA-c5fp-p67m-gq56. The library is documented as a one-to-one substitute for KnpLabs/snappy and shares the same code shape.
Affected versions
pontedilana/php-weasyprint versions <= 2.5.1.
Patched in: 2.6.0.
Privilege required
Any caller that can influence the attachment option value handed to Pdf::generate() / Pdf::getOutput() / setOption('attachment', ...). Typical reach paths: a value sourced from a request parameter, a per-tenant configuration row, or any user-controllable field that flows into the attachment list.
Vulnerable code
src/Pdf.php — isOptionUrl() accepts any well-formed URL regardless of scheme:
protected function isOptionUrl($option): bool
{
return false !== \filter_var($option, \FILTER_VALIDATE_URL);
}
src/Pdf.php — handleArrayOptions() fetches the URL content for the attachment option:
$fetchUrlContent = 'attachment' === $option && $this->isOptionUrl($item);
if ($saveToTempFile || $fetchUrlContent) {
$fileContent = $fetchUrlContent ? \file_get_contents($item) : $item;
$returnOptions[] = $this->createTemporaryFile($fileContent, $this->optionsWithContentCheck[$option] ?? 'temp');
}
FILTER_VALIDATE_URL returns truthy for http://, https://, ftp://, file://localhost/..., and php://filter/..., so \file_get_contents() is invoked on attacker-chosen schemes with no allow-list.
Proof of concept
<?php
use Pontedilana\PhpWeasyPrint\Pdf;
$pdf = new Pdf('/usr/local/bin/weasyprint');
// Attacker-controlled attachment value (e.g. from a request / tenant config):
// SSRF: http://169.254.169.254/latest/meta-data/iam/security-credentials/
// Local file read: php://filter/convert.base64-encode/resource=/etc/passwd
$attachment = $_GET['doc'];
$pdf->generate('page.html', 'out.pdf', [
'attachment' => $attachment,
]);
// The bytes fetched server-side by file_get_contents() are embedded in out.pdf,
// allowing the attacker to read internal HTTP responses or local files.
Impact
- SSRF: the server fetches arbitrary
http(s)/ftpURLs, reaching internal-only services, link-local metadata endpoints, etc. - Local file / wrapper disclosure:
php://filter/...(and similar) let an attacker read and exfiltrate local file content inside the generated PDF. - Affects any consumer that does not fully control the
attachmentoption value.
Note: passing a plain local path (e.g. /etc/passwd) or a file:// path that resolves to an existing file is handled as a normal local attachment and is not the issue addressed here — that is the documented local-attachment feature (callers must not pass untrusted input to the option). The fix specifically removes the server-side fetch amplification through non-http(s) schemes.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (6.5, Medium) — adjust PR/S/A to the consuming application's reachability (e.g. PR:N if the attachment value is reachable from an unauthenticated surface).
CWE-918 (Server-Side Request Forgery); secondary CWE-22 (Improper Limitation of a Pathname) for the wrapper-based file read.
Suggested fix
Restrict the schemes the library will fetch to an allow-list (http, https by default), and treat any other scheme as inline content instead of fetching it:
private array $allowedSchemes = ['http', 'https'];
// new optional 4th constructor argument: ?array $allowedSchemes = null
protected function isOptionUrl($option): bool
{
$url = \parse_url((string)$option);
return false !== $url
&& isset($url['scheme'])
&& \in_array(\strtolower($url['scheme']), $this->allowedSchemes, true);
}
A value with a non-allowed scheme (file://, php://, ftp://, ...) is then never passed to file_get_contents().
Credit
Reported upstream to KnpLabs/snappy (GHSA-c5fp-p67m-gq56); identified as applicable to pontedilana/php-weasyprint, which mirrors the same code.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | pontedilana/php-weasyprint | all versions | 2.6.0composer require pontedilana/php-weasyprint:^2.6.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pontedilana/php-weasyprint, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update pontedilana/php-weasyprint to 2.6.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-x8g9-h984-pc36 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-x8g9-h984-pc36 can be triaged on real exposure rather than presence alone.
Tailored to GHSA-x8g9-h984-pc36. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-x8g9-h984-pc36 in your dependencies?
O3 Security finds GHSA-x8g9-h984-pc36 across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.