Skip to content

Commit

Permalink
Update resource URI validation and handling
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bsweeney committed Jun 14, 2022
1 parent 5abe328 commit 99aeec1
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 268 deletions.
69 changes: 23 additions & 46 deletions src/Css/Stylesheet.php
Expand Up @@ -325,46 +325,26 @@ function load_css_file($file, $origin = self::ORIG_AUTHOR)
$parsed = Helpers::parse_data_uri($file);
$css = $parsed["data"];
} else {
$parsed_url = Helpers::explode_url($file);

[$this->_protocol, $this->_base_host, $this->_base_path, $filename] = $parsed_url;
$options = $this->_dompdf->getOptions();

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

$options = $this->_dompdf->getOptions();
// Download the remote file
if (!$options->isRemoteEnabled() && ($this->_protocol !== "" && $this->_protocol !== "file://")) {
Helpers::record_warnings(E_USER_WARNING, "Remote CSS resource '$file' referenced, but remote file download is disabled.", __FILE__, __LINE__);
return;
}
if ($this->_protocol === "" || $this->_protocol === "file://") {
$realfile = realpath($file);

$rootDir = realpath($options->getRootDir());
if (strpos($realfile, $rootDir) !== 0) {
$chroot = $options->getChroot();
$chrootValid = false;
foreach ($chroot as $chrootPath) {
$chrootPath = realpath($chrootPath);
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
$chrootValid = true;
break;
}
}
if ($chrootValid !== true) {
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__);
if ($file !== $this->getDefaultStylesheet()) {
$allowed_protocols = $options->getAllowedProtocols();
if (!array_key_exists($protocol, $allowed_protocols)) {
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $file. The communication protocol is not supported.", __FILE__, __LINE__);
return;
}
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($file);
if (!$result) {
Helpers::record_warnings(E_USER_WARNING, "Error loading $file: $message", __FILE__, __LINE__);
return;
}
}

if (!$realfile) {
Helpers::record_warnings(E_USER_WARNING, "File '$realfile' not found.", __FILE__, __LINE__);
return;
}

$file = $realfile;
}

[$css, $http_response_header] = Helpers::getFileContent($file, $this->_dompdf->getHttpContext());

$good_mime_type = true;
Expand All @@ -379,11 +359,12 @@ function load_css_file($file, $origin = self::ORIG_AUTHOR)
}
}
}

if (!$good_mime_type || $css === null) {
Helpers::record_warnings(E_USER_WARNING, "Unable to load css file $file", __FILE__, __LINE__);
return;
}

[$this->_protocol, $this->_base_host, $this->_base_path] = $parsed_url;
}

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

// Resolve the url now in the context of the current stylesheet
$parsed_url = Helpers::explode_url($val);
$path = Helpers::build_url($this->_protocol,
$this->_base_host,
$this->_base_path,
$val);
if (($parsed_url["protocol"] === "" || $parsed_url["protocol"] === "file://") && ($this->_protocol === "" || $this->_protocol === "file://")) {
$path = realpath($path);
// If realpath returns FALSE then specifically state that there is no background image
if ($path === false) {
$path = "none";
}
if ($path === null) {
$path = "none";
}
}
if ($DEBUGCSS) {
$parsed_url = Helpers::explode_url($path);
print "<pre>[_image\n";
print_r($parsed_url);
print $this->_protocol . "\n" . $this->_base_path . "\n" . $path . "\n";
Expand Down Expand Up @@ -1483,9 +1460,9 @@ private function _parse_import($url)
// Above does not work for subfolders and absolute urls.
// Todo: As above, do we need to replace php or file to an empty protocol for local files?
$url = $this->resolve_url($url);

$this->load_css_file($url);
if (($url = $this->resolve_url($url)) !== "none") {
$this->load_css_file($url);
}

// Restore the current base url
$this->_protocol = $protocol;
Expand Down Expand Up @@ -1675,7 +1652,7 @@ public function getDefaultStylesheet()
{
$options = $this->_dompdf->getOptions();
$rootDir = realpath($options->getRootDir());
return $rootDir . self::DEFAULT_STYLESHEET;
return Helpers::build_url("file://", "", $rootDir, $rootDir . self::DEFAULT_STYLESHEET);
}

/**
Expand Down
59 changes: 18 additions & 41 deletions src/Dompdf.php
Expand Up @@ -196,16 +196,6 @@ class Dompdf
*/
private $quirksmode = false;

/**
* Protocol whitelist
*
* Protocols and PHP wrappers allowed in URLs. Full support is not
* guaranteed for the protocols/wrappers contained in this array.
*
* @var array
*/
private $allowedProtocols = ["", "file://", "http://", "https://"];

/**
* Local file extension whitelist
*
Expand Down Expand Up @@ -271,8 +261,11 @@ public function __construct($options = null)
}

$versionFile = realpath(__DIR__ . '/../VERSION');
if (file_exists($versionFile) && ($version = trim(file_get_contents($versionFile))) !== false && $version !== '$Format:<%h>$') {
$this->version = sprintf('dompdf %s', $version);
if (($version = file_get_contents($versionFile)) !== false) {
$version = trim($version);
if ($version !== '$Format:<%h>$') {
$this->version = sprintf('dompdf %s', $version);
}
}

$this->setPhpConfig();
Expand Down Expand Up @@ -352,43 +345,25 @@ public function loadHtmlFile($file, $encoding = null)
[$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($file);
}
$protocol = strtolower($this->protocol);

$uri = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $file);

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

if (!$this->options->isRemoteEnabled() && ($protocol !== "" && $protocol !== "file://")) {
throw new Exception("Remote file requested, but remote file download is disabled.");
}

if ($protocol === "" || $protocol === "file://") {
$realfile = realpath($uri);

$chroot = $this->options->getChroot();
$chrootValid = false;
foreach ($chroot as $chrootPath) {
$chrootPath = realpath($chrootPath);
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
$chrootValid = true;
break;
}
}
if ($chrootValid !== true) {
throw new Exception("Permission denied on $file. The file could not be found under the paths specified by Options::chroot.");
}

$ext = strtolower(pathinfo($realfile, PATHINFO_EXTENSION));
if ($protocol === "file://") {
$ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION));
if (!in_array($ext, $this->allowedLocalFileExtensions)) {
throw new Exception("Permission denied on $file. This file extension is forbidden");
throw new Exception("Permission denied on $file: The file extension is forbidden.");
}
}

if (!$realfile) {
throw new Exception("File '$file' not found.");
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($uri);
if (!$result) {
throw new Exception("Error loading $file: $message");
}

$uri = $realfile;
}

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

$this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
if ($url !== null) {
$this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
}
}
break;

Expand Down
35 changes: 8 additions & 27 deletions src/FontMetrics.php
Expand Up @@ -214,37 +214,18 @@ public function registerFont($style, $remoteFile, $context = null)

// Download the remote file
[$protocol] = Helpers::explode_url($remoteFile);
if (!$this->options->isRemoteEnabled() && ($protocol !== "" && $protocol !== "file://")) {
Helpers::record_warnings(E_USER_WARNING, "Remote font resource $remoteFile referenced, but remote file download is disabled.", __FILE__, __LINE__);
return false;
$allowed_protocols = $this->options->getAllowedProtocols();
if (!array_key_exists($protocol, $allowed_protocols)) {
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__);
}
if ($protocol === "" || $protocol === "file://") {
$realfile = realpath($remoteFile);

$rootDir = realpath($this->options->getRootDir());
if (strpos($realfile, $rootDir) !== 0) {
$chroot = $this->options->getChroot();
$chrootValid = false;
foreach ($chroot as $chrootPath) {
$chrootPath = realpath($chrootPath);
if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
$chrootValid = true;
break;
}
}
if ($chrootValid !== true) {
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__);
return false;
}
}

if (!$realfile) {
Helpers::record_warnings(E_USER_WARNING, "File '$realfile' not found.", __FILE__, __LINE__);
return false;
foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($remoteFile);
if ($result !== true) {
Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
}

$remoteFile = $realfile;
}

list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
if ($remoteFileContent === null) {
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/FrameDecorator/Image.php
Expand Up @@ -57,7 +57,7 @@ function __construct(Frame $frame, Dompdf $dompdf)
$dompdf->getProtocol(),
$dompdf->getBaseHost(),
$dompdf->getBasePath(),
$dompdf
$dompdf->getOptions()
);

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

0 comments on commit 99aeec1

Please sign in to comment.