Files
InvoiceShelf/app/Services/PDFDrivers/GotenbergPDFDriver.php
Darko Gjorgjijoski d615cfb4ff fix(security): block SSRF via the Gotenberg host setting (GHSA-mfxg) (#664)
gotenberg_host was validated only with Laravel's 'url' rule, which permits
loopback/private/link-local hosts (e.g. http://127.0.0.1, http://10.0.0.1,
the cloud metadata endpoint http://169.254.169.254). When a PDF renders, the
server POSTs the document HTML to that host — an SSRF primitive.

- Adds App\Rules\SafeRemoteUrl: requires http(s) and rejects any host that
  resolves to a loopback/private/link-local/CGNAT/reserved address (IPv4 and
  IPv6), including literal-IP hosts.
- Wires it into PDFConfigurationRequest for gotenberg_host.
- Adds a defensive re-check in GotenbergPDFDriver before the outbound call to
  cover hosts set via env/seed/stale config or DNS rebinding (TOCTOU).

Adds unit tests for the rule + validator integration.
2026-06-12 09:18:57 +02:00

70 lines
2.0 KiB
PHP

<?php
namespace App\Services\PDFDrivers;
use App\Rules\SafeRemoteUrl;
use Gotenberg\Gotenberg;
use Gotenberg\Stream;
use Illuminate\Http\Response;
use Psr\Http\Message\ResponseInterface;
class GotenbergPDFResponse
{
/** @var ResponseInterface */
protected $response;
public function __construct($stream)
{
$this->response = $stream;
}
public function stream(string $filename = 'document.pdf'): Response
{
$output = $this->response->getBody();
return new Response($output, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
]);
}
public function output(): string
{
return $this->response->getBody()->getContents();
}
}
class GotenbergPDFDriver
{
public function loadView(string $viewname): GotenbergPDFResponse
{
$papersize = explode(' ', config('pdf.connections.gotenberg.papersize'));
if (count($papersize) != 2) {
throw new \InvalidArgumentException('Invalid Gotenberg Papersize specified');
}
$host = config('pdf.connections.gotenberg.host');
// Defense in depth against SSRF: a host that bypassed request-time
// validation (env/seed/stale config, or DNS rebinding) must still not
// be able to target internal/private addresses.
if (! SafeRemoteUrl::isSafe((string) $host)) {
throw new \RuntimeException('Refusing to render PDF: unsafe Gotenberg host.');
}
$request = Gotenberg::chromium($host)
->pdf()
->margins(0, 0, 0, 0) // Margins can be set using CSS
->paperSize($papersize[0], $papersize[1])
->html(
Stream::string(
'document.html',
view($viewname)->render(),
)
);
$result = Gotenberg::send($request);
return new GotenbergPDFResponse($result);
}
}