{"record":{"id":"0476a30c1b06db18","repo":"dompdf/dompdf","slug":"error-loading-file-message","errorCode":null,"errorMessage":"Error loading $file: $message","messagePattern":"Error loading \\$file: \\$message","errorType":"exception","errorClass":"Dompdf\\Exception","httpStatus":null,"severity":"error","filePath":"src/Dompdf.php","lineNumber":370,"sourceCode":"        $protocol = strtolower($this->protocol);\n        $uri = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $file, $this->options->getChroot());\n\n        $allowed_protocols = $this->options->getAllowedProtocols();\n        if (!array_key_exists($protocol, $allowed_protocols)) {\n            throw new Exception(\"Permission denied on $file. The communication protocol is not supported.\");\n        }\n\n        if ($protocol === \"file://\") {\n            $ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION));\n            if (!in_array($ext, $this->allowedLocalFileExtensions)) {\n                throw new Exception(\"Permission denied on $file: The file extension is forbidden.\");\n            }\n        }\n\n        foreach ($allowed_protocols[$protocol][\"rules\"] as $rule) {\n            [$result, $message] = $rule($uri);\n            if (!$result) {\n                throw new Exception(\"Error loading $file: $message\");\n            }\n        }\n\n        [$contents, $http_response_header] = Helpers::getFileContent($uri, $this->options->getHttpContext());\n        if ($contents === null) {\n            throw new Exception(\"File '$file' not found.\");\n        }\n\n        // See http://the-stickman.com/web-development/php/getting-http-response-headers-when-using-file_get_contents/\n        if (isset($http_response_header)) {\n            foreach ($http_response_header as $_header) {\n                if (preg_match(\"@Content-Type:\\s*[\\w/]+;\\s*?charset=([^\\s]+)@i\", $_header, $matches)) {\n                    $encoding = strtoupper($matches[1]);\n                    break;\n                }\n            }\n        }\n","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/dompdf/dompdf/blob/b14267808b811db092f53830f81f4706f4917c79/src/Dompdf.php#L352-L388","documentation":"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.","triggerScenarios":"$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.","commonSituations":"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.","solutions":["Whitelist the directories you actually load from: $dompdf->getOptions()->setChroot(['/var/www/site/templates']).","Place the HTML under an already-allowed root or reference it relative to that root.","Or fetch the content yourself (file_get_contents within your own security policy) and call loadHtml().","Never widen chroot for user-supplied paths — validate them against your own allowlist first."],"exampleFix":"// before\n$dompdf->loadHtmlFile('/var/www/app/templates/invoice.html');\n// throws: file not under default chroot (dompdf's own directory)\n\n// after\n$dompdf->getOptions()->setChroot(['/var/www/app/templates']);\n$dompdf->loadHtmlFile('/var/www/app/templates/invoice.html');","handlingStrategy":"validation","validationCode":"$chroots = $dompdf->getOptions()->getChroot(); // array of allowed roots\n$real = realpath($file);\n$ok = $real !== false;\nforeach ($chroots as $root) {\n    $r = realpath($root);\n    if ($r === false || !$ok || strpos($real, $r . DIRECTORY_SEPARATOR) !== 0 && $real !== $r) {\n        continue;\n    }\n    $ok = $ok && true;\n    break;\n}\nif (!$ok) {\n    throw new InvalidArgumentException(\"File outside dompdf chroot: $file\");\n}\n$dompdf->loadHtmlFile($file);","typeGuard":null,"tryCatchPattern":"try {\n    $dompdf->loadHtmlFile($file);\n} catch (\\Dompdf\\Exception $e) {\n    if (strpos($e->getMessage(), 'Options::chroot') !== false) {\n        // either the file is genuinely disallowed (reject), or loadHtml() after your own validation\n    }\n    throw $e;\n}","preventionTips":["Configure Options::setChroot() explicitly to your template directories at bootstrap.","Validate user-supplied paths against your own allowlist before they reach dompdf.","Beware symlinks and case differences on macOS/Windows when reasoning about chroot membership."],"tags":["security","chroot","path-traversal","file","configuration"],"backgroundTag":"path-outside-chroot","analyzedSha":"b14267808b811db092f53830f81f4706f4917c79","analyzedAt":"2026-08-21T03:01:11.111Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}