|
| 1 | +<?php |
| 2 | + |
| 3 | +/* For licensing terms, see /license.txt */ |
| 4 | + |
| 5 | +declare(strict_types=1); |
| 6 | + |
| 7 | +namespace Chamilo\CoreBundle\Component\Mpdf; |
| 8 | + |
| 9 | +use Chamilo\CoreBundle\Helpers\SafeHttpClientHelper; |
| 10 | +use Mpdf\Container\SimpleContainer; |
| 11 | +use Mpdf\Http\ClientInterface; |
| 12 | +use Mpdf\PsrHttpMessageShim\Response; |
| 13 | +use Mpdf\PsrHttpMessageShim\Stream; |
| 14 | +use Psr\Http\Message\RequestInterface; |
| 15 | +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; |
| 16 | + |
| 17 | +/** |
| 18 | + * SSRF-safe HTTP client for mPDF (advisory 22.5). |
| 19 | + * |
| 20 | + * When rendering user-authored HTML to PDF, mPDF fetches remote `<img src>` / |
| 21 | + * `@import url()` server-side through its AssetFetcher; the bundled cURL/socket |
| 22 | + * clients have no private-IP filtering, so a student embedding |
| 23 | + * `<img src="http://169.254.169.254/...">` in a portfolio item and exporting to |
| 24 | + * PDF triggers a server-side request to an internal host. Passing |
| 25 | + * `new Mpdf($config, SafeMpdfHttpClient::container())` routes every remote asset |
| 26 | + * fetch through SafeHttpClientHelper, which refuses loopback/private/reserved |
| 27 | + * targets, validates each redirect hop and speaks only http(s). Public images |
| 28 | + * keep loading unchanged. |
| 29 | + */ |
| 30 | +final class SafeMpdfHttpClient implements ClientInterface |
| 31 | +{ |
| 32 | + /** |
| 33 | + * Ready-to-use mPDF container that overrides only the HTTP client. |
| 34 | + */ |
| 35 | + public static function container(): SimpleContainer |
| 36 | + { |
| 37 | + return new SimpleContainer(['httpClient' => new self()]); |
| 38 | + } |
| 39 | + |
| 40 | + public function sendRequest(RequestInterface $request): Response |
| 41 | + { |
| 42 | + $response = new Response(); |
| 43 | + |
| 44 | + try { |
| 45 | + $remote = SafeHttpClientHelper::create()->request('GET', (string) $request->getUri()); |
| 46 | + $statusCode = $remote->getStatusCode(); |
| 47 | + // false: read the body regardless of HTTP status (AssetFetcher checks 2xx itself). |
| 48 | + $content = $remote->getContent(false); |
| 49 | + } catch (ExceptionInterface $e) { |
| 50 | + // Blocked, unreachable or transport error: empty response so mPDF skips the asset. |
| 51 | + return $response; |
| 52 | + } |
| 53 | + |
| 54 | + return $response |
| 55 | + ->withStatus($statusCode) |
| 56 | + ->withBody(Stream::create($content)) |
| 57 | + ; |
| 58 | + } |
| 59 | +} |
0 commit comments