GHSA-x8g9-h984-pc36

Suggest an improvement
Source
https://github.com/advisories/GHSA-x8g9-h984-pc36
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-x8g9-h984-pc36/GHSA-x8g9-h984-pc36.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-x8g9-h984-pc36
Aliases
  • CVE-2026-49359
Published
2026-06-26T22:11:40Z
Modified
2026-06-26T22:15:08.665146341Z
Severity
  • 6.5 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N CVSS Calculator
Summary
PhpWeasyPrint vulnerable to SSRF and local file disclosure via the attachment option
Details

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.phpisOptionUrl() accepts any well-formed URL regardless of scheme:

protected function isOptionUrl($option): bool
{
    return false !== \filter_var($option, \FILTER_VALIDATE_URL);
}

src/Pdf.phphandleArrayOptions() 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)/ftp URLs, 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 attachment option 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.

Database specific
{
    "github_reviewed_at": "2026-06-26T22:11:40Z",
    "nvd_published_at": "2026-06-19T18:16:19Z",
    "github_reviewed": true,
    "cwe_ids": [
        "CWE-918"
    ],
    "severity": "MODERATE"
}
References

Affected packages

Packagist / pontedilana/php-weasyprint

Package

Name
pontedilana/php-weasyprint
Purl
pkg:composer/pontedilana%2Fphp-weasyprint

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0Unknown introduced version / All previous versions are affected
Fixed
2.6.0

Affected versions

0.*
0.9.0
0.10.0
0.10.1
0.11.0
0.12.0
0.13.0
1.*
1.0.0
1.0.1
1.1.0
1.1.1
1.2.0
1.3.0
1.4.0
1.5.0
2.*
2.0.0
2.1.0
2.2.0
2.3.0
2.4.0
2.5.0
2.5.1

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-x8g9-h984-pc36/GHSA-x8g9-h984-pc36.json"
last_known_affected_version_range
"<= 2.5.1"