dompdf/dompdf · error · Dompdf\Exception

Error loading $file: $message

Error message

Error loading $file: $message

What it means

Every allowed protocol carries validation rules executed before loading; for file:// the default rule is the chroot check, which rejects any resolved path outside the directories configured in Options::chroot (by default the dompdf library directory). This exception wraps the failing rule's message — most commonly 'Permission denied. The file could not be found under the paths specified by Options::chroot.' It protects against path traversal/local file inclusion when rendering untrusted HTML.

Source

Thrown at src/Dompdf.php:370

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

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

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

        [$contents, $http_response_header] = Helpers::getFileContent($uri, $this->options->getHttpContext());
        if ($contents === null) {
            throw new Exception("File '$file' not found.");
        }

        // See http://the-stickman.com/web-development/php/getting-http-response-headers-when-using-file_get_contents/
        if (isset($http_response_header)) {
            foreach ($http_response_header as $_header) {
                if (preg_match("@Content-Type:\s*[\w/]+;\s*?charset=([^\s]+)@i", $_header, $matches)) {
                    $encoding = strtoupper($matches[1]);
                    break;
                }
            }
        }

View on GitHub (pinned to b14267808b)

Solutions

  1. Whitelist the directories you actually load from: $dompdf->getOptions()->setChroot(['/var/www/site/templates']).
  2. Place the HTML under an already-allowed root or reference it relative to that root.
  3. Or fetch the content yourself (file_get_contents within your own security policy) and call loadHtml().
  4. Never widen chroot for user-supplied paths — validate them against your own allowlist first.

Example fix

// before
$dompdf->loadHtmlFile('/var/www/app/templates/invoice.html');
// throws: file not under default chroot (dompdf's own directory)

// after
$dompdf->getOptions()->setChroot(['/var/www/app/templates']);
$dompdf->loadHtmlFile('/var/www/app/templates/invoice.html');
Defensive patterns

Strategy: validation

Validate before calling

$chroots = $dompdf->getOptions()->getChroot(); // array of allowed roots
$real = realpath($file);
$ok = $real !== false;
foreach ($chroots as $root) {
    $r = realpath($root);
    if ($r === false || !$ok || strpos($real, $r . DIRECTORY_SEPARATOR) !== 0 && $real !== $r) {
        continue;
    }
    $ok = $ok && true;
    break;
}
if (!$ok) {
    throw new InvalidArgumentException("File outside dompdf chroot: $file");
}
$dompdf->loadHtmlFile($file);

Try / catch

try {
    $dompdf->loadHtmlFile($file);
} catch (\Dompdf\Exception $e) {
    if (strpos($e->getMessage(), 'Options::chroot') !== false) {
        // either the file is genuinely disallowed (reject), or loadHtml() after your own validation
    }
    throw $e;
}

Prevention

When it happens

Trigger: $dompdf->loadHtmlFile('/var/www/site/templates/x.html') where the chroot still points at dompdf's own directory; absolute paths on systems where the default chroot does not cover them; symlinks resolving outside the chroot; Windows paths where realpath/case handling differs.

Common situations: Loading app templates without ever configuring chroot; deployments that move dompdf via composer so the default root changes; hardening reviews that tighten (but misconfigure) chroot; mixed absolute/relative path handling in the calling code.

Related errors


AI-assisted analysis of dompdf/dompdf@b14267808b (2026-08-21). Data as JSON: /api/errors/0476a30c1b06db18. Report an issue: GitHub.