Skip to content

Commit ada53f3

Browse files
AngelFQCywarnier
authored andcommitted
Security: prevent SSRF in mPDF remote asset fetch via injected IP-filtered HTTP clien
See advisory GHSA-6xgw-x5v2-mhx7
1 parent 71d13d0 commit ada53f3

5 files changed

Lines changed: 69 additions & 4 deletions

File tree

public/main/inc/lib/pdf.lib.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/* See license terms in /license.txt */
44

5+
use Chamilo\CoreBundle\Component\Mpdf\SafeMpdfHttpClient;
56
use Chamilo\CoreBundle\Framework\Container;
67
use Masterminds\HTML5;
78
use Mpdf\HTMLParserMode;
@@ -85,7 +86,9 @@ public function __construct(
8586
if ($value) {
8687
$params['img_dpi'] = $value;
8788
}
88-
$this->pdf = new Mpdf($params);
89+
// SSRF protection: route mPDF's remote asset fetches through an
90+
// IP-filtered HTTP client (blocks loopback/private/reserved/metadata).
91+
$this->pdf = new Mpdf($params, SafeMpdfHttpClient::container());
8992
}
9093

9194
/**
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
}

src/CoreBundle/Controller/AttendanceController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Chamilo\CoreBundle\Controller;
66

7+
use Chamilo\CoreBundle\Component\Mpdf\SafeMpdfHttpClient;
78
use Chamilo\CoreBundle\Entity\Course;
89
use Chamilo\CoreBundle\Entity\CourseRelUser;
910
use Chamilo\CoreBundle\Entity\Session;
@@ -296,7 +297,7 @@ public function exportToPdf(int $id, Request $request): Response
296297
$mpdf = new Mpdf([
297298
'orientation' => 'L',
298299
'tempDir' => api_get_path(SYS_ARCHIVE_PATH).'mpdf/',
299-
]);
300+
], SafeMpdfHttpClient::container());
300301
$mpdf->WriteHTML($html);
301302

302303
return new Response(

src/CoreBundle/Controller/CertificateController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace Chamilo\CoreBundle\Controller;
88

9+
use Chamilo\CoreBundle\Component\Mpdf\SafeMpdfHttpClient;
910
use Chamilo\CoreBundle\Entity\GradebookCertificate;
1011
use Chamilo\CoreBundle\Entity\User;
1112
use Chamilo\CoreBundle\Framework\Container;
@@ -69,7 +70,7 @@ public function downloadPdf(string $hash): Response
6970
$mpdf = new Mpdf([
7071
'format' => 'A4',
7172
'tempDir' => api_get_path(SYS_ARCHIVE_PATH).'mpdf/',
72-
]);
73+
], SafeMpdfHttpClient::container());
7374
$mpdf->WriteHTML($html);
7475
$pdfBinary = $mpdf->Output('', Destination::STRING_RETURN);
7576

src/CoreBundle/Controller/StudentPublicationController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
use Chamilo\CoreBundle\AiProvider\AiProviderFactory;
1010
use Chamilo\CoreBundle\AiProvider\AiTaskGraderService;
11+
use Chamilo\CoreBundle\Component\Mpdf\SafeMpdfHttpClient;
1112
use Chamilo\CoreBundle\Entity\CourseRelUser;
1213
use Chamilo\CoreBundle\Entity\ResourceNode;
1314
use Chamilo\CoreBundle\Entity\Session;
@@ -387,7 +388,7 @@ public function exportPdf(
387388
try {
388389
$mpdf = new Mpdf([
389390
'tempDir' => api_get_path(SYS_ARCHIVE_PATH).'mpdf/',
390-
]);
391+
], SafeMpdfHttpClient::container());
391392
$mpdf->WriteHTML($html);
392393

393394
return new Response(

0 commit comments

Comments
 (0)