Skip to content

Commit 99aeec1

Browse files
committed
Update resource URI validation and handling
URI scheme (protocol) validation rules are now specified through the Options class. By default file and http(s) URIs are allowed and validation rules defined. Validation rules for PHAR URIs are defined but the scheme is not enabled by default. Resource retrieval has been updated to use file_get_contents for schemes other than http(s). fixes #621 fixes #2826 in lieu of #1903
1 parent 5abe328 commit 99aeec1

File tree

11 files changed

+359
-268
lines changed

11 files changed

+359
-268
lines changed

src/Css/Stylesheet.php

+23-46
Original file line numberDiff line numberDiff line change
@@ -325,46 +325,26 @@ function load_css_file($file, $origin = self::ORIG_AUTHOR)
325325
$parsed = Helpers::parse_data_uri($file);
326326
$css = $parsed["data"];
327327
} else {
328-
$parsed_url = Helpers::explode_url($file);
329-
330-
[$this->_protocol, $this->_base_host, $this->_base_path, $filename] = $parsed_url;
328+
$options = $this->_dompdf->getOptions();
331329

332-
$file = Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $filename);
330+
$parsed_url = Helpers::explode_url($file);
331+
$protocol = $parsed_url["protocol"];
333332

334-
$options = $this->_dompdf->getOptions();
335-
// Download the remote file
336-
if (!$options->isRemoteEnabled() && ($this->_protocol !== "" && $this->_protocol !== "file://")) {
337-
Helpers::record_warnings(E_USER_WARNING, "Remote CSS resource '$file' referenced, but remote file download is disabled.", __FILE__, __LINE__);
338-
return;
339-
}
340-
if ($this->_protocol === "" || $this->_protocol === "file://") {
341-
$realfile = realpath($file);
342-
343-
$rootDir = realpath($options->getRootDir());
344-
if (strpos($realfile, $rootDir) !== 0) {
345-
$chroot = $options->getChroot();
346-
$chrootValid = false;
347-
foreach ($chroot as $chrootPath) {
348-
$chrootPath = realpath($chrootPath);
349-
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
350-
$chrootValid = true;
351-
break;
352-
}
353-
}
354-
if ($chrootValid !== true) {
355-
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $file. The file could not be found under the paths specified by Options::chroot.", __FILE__, __LINE__);
333+
if ($file !== $this->getDefaultStylesheet()) {
334+
$allowed_protocols = $options->getAllowedProtocols();
335+
if (!array_key_exists($protocol, $allowed_protocols)) {
336+
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $file. The communication protocol is not supported.", __FILE__, __LINE__);
337+
return;
338+
}
339+
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
340+
[$result, $message] = $rule($file);
341+
if (!$result) {
342+
Helpers::record_warnings(E_USER_WARNING, "Error loading $file: $message", __FILE__, __LINE__);
356343
return;
357344
}
358345
}
359-
360-
if (!$realfile) {
361-
Helpers::record_warnings(E_USER_WARNING, "File '$realfile' not found.", __FILE__, __LINE__);
362-
return;
363-
}
364-
365-
$file = $realfile;
366346
}
367-
347+
368348
[$css, $http_response_header] = Helpers::getFileContent($file, $this->_dompdf->getHttpContext());
369349

370350
$good_mime_type = true;
@@ -379,11 +359,12 @@ function load_css_file($file, $origin = self::ORIG_AUTHOR)
379359
}
380360
}
381361
}
382-
383362
if (!$good_mime_type || $css === null) {
384363
Helpers::record_warnings(E_USER_WARNING, "Unable to load css file $file", __FILE__, __LINE__);
385364
return;
386365
}
366+
367+
[$this->_protocol, $this->_base_host, $this->_base_path] = $parsed_url;
387368
}
388369

389370
$this->_parse_css($css);
@@ -1421,20 +1402,16 @@ public function resolve_url($val): string
14211402
$val = preg_replace("/url\(\s*['\"]?([^'\")]+)['\"]?\s*\)/", "\\1", trim($val));
14221403

14231404
// Resolve the url now in the context of the current stylesheet
1424-
$parsed_url = Helpers::explode_url($val);
14251405
$path = Helpers::build_url($this->_protocol,
14261406
$this->_base_host,
14271407
$this->_base_path,
14281408
$val);
1429-
if (($parsed_url["protocol"] === "" || $parsed_url["protocol"] === "file://") && ($this->_protocol === "" || $this->_protocol === "file://")) {
1430-
$path = realpath($path);
1431-
// If realpath returns FALSE then specifically state that there is no background image
1432-
if ($path === false) {
1433-
$path = "none";
1434-
}
1409+
if ($path === null) {
1410+
$path = "none";
14351411
}
14361412
}
14371413
if ($DEBUGCSS) {
1414+
$parsed_url = Helpers::explode_url($path);
14381415
print "<pre>[_image\n";
14391416
print_r($parsed_url);
14401417
print $this->_protocol . "\n" . $this->_base_path . "\n" . $path . "\n";
@@ -1483,9 +1460,9 @@ private function _parse_import($url)
14831460
// Above does not work for subfolders and absolute urls.
14841461
// Todo: As above, do we need to replace php or file to an empty protocol for local files?
14851462
1486-
$url = $this->resolve_url($url);
1487-
1488-
$this->load_css_file($url);
1463+
if (($url = $this->resolve_url($url)) !== "none") {
1464+
$this->load_css_file($url);
1465+
}
14891466

14901467
// Restore the current base url
14911468
$this->_protocol = $protocol;
@@ -1675,7 +1652,7 @@ public function getDefaultStylesheet()
16751652
{
16761653
$options = $this->_dompdf->getOptions();
16771654
$rootDir = realpath($options->getRootDir());
1678-
return $rootDir . self::DEFAULT_STYLESHEET;
1655+
return Helpers::build_url("file://", "", $rootDir, $rootDir . self::DEFAULT_STYLESHEET);
16791656
}
16801657

16811658
/**

src/Dompdf.php

+18-41
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,6 @@ class Dompdf
196196
*/
197197
private $quirksmode = false;
198198

199-
/**
200-
* Protocol whitelist
201-
*
202-
* Protocols and PHP wrappers allowed in URLs. Full support is not
203-
* guaranteed for the protocols/wrappers contained in this array.
204-
*
205-
* @var array
206-
*/
207-
private $allowedProtocols = ["", "file://", "http://", "https://"];
208-
209199
/**
210200
* Local file extension whitelist
211201
*
@@ -271,8 +261,11 @@ public function __construct($options = null)
271261
}
272262

273263
$versionFile = realpath(__DIR__ . '/../VERSION');
274-
if (file_exists($versionFile) && ($version = trim(file_get_contents($versionFile))) !== false && $version !== '$Format:<%h>$') {
275-
$this->version = sprintf('dompdf %s', $version);
264+
if (($version = file_get_contents($versionFile)) !== false) {
265+
$version = trim($version);
266+
if ($version !== '$Format:<%h>$') {
267+
$this->version = sprintf('dompdf %s', $version);
268+
}
276269
}
277270

278271
$this->setPhpConfig();
@@ -352,43 +345,25 @@ public function loadHtmlFile($file, $encoding = null)
352345
[$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($file);
353346
}
354347
$protocol = strtolower($this->protocol);
355-
356348
$uri = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $file);
357349

358-
if (!in_array($protocol, $this->allowedProtocols, true)) {
350+
$allowed_protocols = $this->options->getAllowedProtocols();
351+
if (!array_key_exists($protocol, $allowed_protocols)) {
359352
throw new Exception("Permission denied on $file. The communication protocol is not supported.");
360353
}
361354

362-
if (!$this->options->isRemoteEnabled() && ($protocol !== "" && $protocol !== "file://")) {
363-
throw new Exception("Remote file requested, but remote file download is disabled.");
364-
}
365-
366-
if ($protocol === "" || $protocol === "file://") {
367-
$realfile = realpath($uri);
368-
369-
$chroot = $this->options->getChroot();
370-
$chrootValid = false;
371-
foreach ($chroot as $chrootPath) {
372-
$chrootPath = realpath($chrootPath);
373-
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
374-
$chrootValid = true;
375-
break;
376-
}
377-
}
378-
if ($chrootValid !== true) {
379-
throw new Exception("Permission denied on $file. The file could not be found under the paths specified by Options::chroot.");
380-
}
381-
382-
$ext = strtolower(pathinfo($realfile, PATHINFO_EXTENSION));
355+
if ($protocol === "file://") {
356+
$ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION));
383357
if (!in_array($ext, $this->allowedLocalFileExtensions)) {
384-
throw new Exception("Permission denied on $file. This file extension is forbidden");
358+
throw new Exception("Permission denied on $file: The file extension is forbidden.");
385359
}
360+
}
386361

387-
if (!$realfile) {
388-
throw new Exception("File '$file' not found.");
362+
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
363+
[$result, $message] = $rule($uri);
364+
if (!$result) {
365+
throw new Exception("Error loading $file: $message");
389366
}
390-
391-
$uri = $realfile;
392367
}
393368

394369
[$contents, $http_response_header] = Helpers::getFileContent($uri, $this->options->getHttpContext());
@@ -604,7 +579,9 @@ private function processHtml()
604579
$url = $tag->getAttribute("href");
605580
$url = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $url);
606581

607-
$this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
582+
if ($url !== null) {
583+
$this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
584+
}
608585
}
609586
break;
610587

src/FontMetrics.php

+8-27
Original file line numberDiff line numberDiff line change
@@ -214,37 +214,18 @@ public function registerFont($style, $remoteFile, $context = null)
214214

215215
// Download the remote file
216216
[$protocol] = Helpers::explode_url($remoteFile);
217-
if (!$this->options->isRemoteEnabled() && ($protocol !== "" && $protocol !== "file://")) {
218-
Helpers::record_warnings(E_USER_WARNING, "Remote font resource $remoteFile referenced, but remote file download is disabled.", __FILE__, __LINE__);
219-
return false;
217+
$allowed_protocols = $this->options->getAllowedProtocols();
218+
if (!array_key_exists($protocol, $allowed_protocols)) {
219+
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__);
220220
}
221-
if ($protocol === "" || $protocol === "file://") {
222-
$realfile = realpath($remoteFile);
223-
224-
$rootDir = realpath($this->options->getRootDir());
225-
if (strpos($realfile, $rootDir) !== 0) {
226-
$chroot = $this->options->getChroot();
227-
$chrootValid = false;
228-
foreach ($chroot as $chrootPath) {
229-
$chrootPath = realpath($chrootPath);
230-
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
231-
$chrootValid = true;
232-
break;
233-
}
234-
}
235-
if ($chrootValid !== true) {
236-
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The file could not be found under the paths specified by Options::chroot.", __FILE__, __LINE__);
237-
return false;
238-
}
239-
}
240221

241-
if (!$realfile) {
242-
Helpers::record_warnings(E_USER_WARNING, "File '$realfile' not found.", __FILE__, __LINE__);
243-
return false;
222+
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
223+
[$result, $message] = $rule($remoteFile);
224+
if ($result !== true) {
225+
Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
244226
}
245-
246-
$remoteFile = $realfile;
247227
}
228+
248229
list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
249230
if ($remoteFileContent === null) {
250231
return false;

src/FrameDecorator/Image.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function __construct(Frame $frame, Dompdf $dompdf)
5757
$dompdf->getProtocol(),
5858
$dompdf->getBaseHost(),
5959
$dompdf->getBasePath(),
60-
$dompdf
60+
$dompdf->getOptions()
6161
);
6262

6363
if (Cache::is_broken($this->_image_url) &&

0 commit comments

Comments
 (0)