mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-16 22:05:20 +00:00
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.
70 lines
2.0 KiB
PHP
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);
|
|
}
|
|
}
|