CVE-2026-55792 Patch Review: Partial Fix for Craft CMS Sandbox-Bypass File Disclosure
Summary
The patch removes the client-side dependency on the deprecated app/resource-js proxy for newly appended control-panel assets by changing script injection behavior in Craft.js. That reduces exposure tied to proxied cross-origin resource loading, but the server-side resource-js endpoint remains present and anonymously accessible in the shown diff. Based on the provided sources, the change appears to mitigate one delivery path rather than conclusively eliminate the underlying authorization and sandbox-bypass risk described in the CVE.
Analysis
Vulnerability
CVE-2026-55792 describes sensitive file disclosure in Craft CMS via a Twig sandbox bypass, where low-privileged users with template customization capability can read local files such as .env, leading to credential and key compromise. The provided patch material from the Craft CMS pull request does not show Twig sandbox policy changes or server-side template authorization hardening. Instead, it focuses on control-panel HTML/script append behavior and deprecates reliance on the app/resource-js proxy.
From the supplied snippets, the relevant risk surface is that appended CP HTML previously rewrote certain script URLs to app/resource-js when the resource URL matched resourceBaseUrl but was not same-host. That created a server-mediated fetch path for JavaScript resources. The vulnerable client logic explicitly proxied such resources:
if (
node.src.startsWith(this.resourceBaseUrl) &&
!this.isSameHost(node.src)
) {
node.src = this.getActionUrl('app/resource-js', {
url: node.src,
});
}The server endpoint remains visible in the patch context and is still marked anonymous in the controller behavior map shown in the snippets, while validating only that the requested URL begins with the asset manager base URL and that the derived resource URI is path-contained. In the non-cached path case, it performs an HTTP GET to the supplied URL:
'resource-js' => self::ALLOW_ANONYMOUS_LIVE | self::ALLOW_ANONYMOUS_OFFLINE,
...
if (!$assetManager->cacheSourcePaths) {
Session::close();
$response = Craft::createGuzzleClient()->get($url);
$this->response->setCacheHeaders();
$this->response->getHeaders()->set('content-type', 'application/javascript');
return $this->asRaw($response->getBody());
}On the evidence provided, the patch addresses a script-loading/proxying mechanism adjacent to the reported issue, but the source excerpts do not demonstrate direct remediation of the Twig sandbox bypass itself. That gap is important because the CVE impact is unauthorized local file read, not merely incorrect script loading semantics. See also the CVE records at CVE.org and NVD.
Patch
The client-side patch in PR #18559 substantially rewrites Craft._appendHtml():
- It changes the function to
async. - It separates script nodes from non-script nodes instead of letting jQuery handle script execution implicitly.
- It preserves execution order by appending native
<script>elements sequentially and awaiting external script load completion. - It removes the vulnerable branch that rewrote cross-host resource URLs through
app/resource-js. - It adds a queue mechanism so concurrent append operations serialize through
_appendHtmlQueue.
The patched logic is materially different:
const scriptNodes = [];
const otherNodes = [];
...
if (node.nodeName === 'SCRIPT') {
if (node.src) {
if (!this._existingJs) {
this._existingJs = $('script[src]')
.toArray()
.map((n) => n.src.replace(/&/g, '&'));
}
if (this._existingJs.includes(node.src)) {
continue;
}
this._existingJs.push(node.src);
}
scriptNodes.push(node);
continue;
}
...
for (const scriptNode of scriptNodes) {
const scriptEl = document.createElement('script');
for (const attr of scriptNode.attributes) {
scriptEl.setAttribute(attr.name, attr.value);
}
if (scriptEl.src) {
await new Promise((resolve) => {
scriptEl.onload = scriptEl.onerror = resolve;
parentEl.appendChild(scriptEl);
});
} else {
scriptEl.textContent = scriptNode.textContent;
parentEl.appendChild(scriptEl);
}
}The queueing change also reduces race conditions in repeated head/body appends:
_appendHtmlQueue: null,
_queueAppendHtml: function (html, $parent) {
const append = () => this._appendHtml(html, $parent);
this._appendHtmlQueue = (this._appendHtmlQueue || Promise.resolve())
.catch(() => {})
.then(append);
return this._appendHtmlQueue;
},However, the server-side actionResourceJs() implementation shown in the provided before/after snippets is functionally unchanged; only the deprecation text is updated. The endpoint still exists in the patch context and still appears anonymously accessible in the controller configuration excerpt. Therefore, the patch's effective security improvement comes from removing current CP asset code's dependency on that endpoint, not from deleting or hardening the endpoint itself.
Review
Pros
- Removes the explicit client-side rewrite of cross-host asset URLs to
app/resource-js, which narrows exposure to server-side proxy behavior for modern CP asset appends. - Uses native script insertion with ordered async waiting, avoiding jQuery's problematic script evaluation path cited in the patch comments and reducing nondeterministic execution behavior.
- Adds append serialization via
_appendHtmlQueue, which is a sound correctness improvement for concurrent UI updates and may prevent subtle state corruption during dynamic CP rendering. - Maintains backward compatibility by leaving
actionResourceJs()available for older assets, reducing upgrade breakage risk.
Cons
- The provided patch evidence does not show any direct fix to the reported root cause: Twig sandbox bypass enabling local file disclosure. No sandbox policy, template capability, or file access control changes are present in the supplied snippets.
- The
app/resource-jsendpoint remains implemented and still appears anonymously accessible in the shown controller behavior map, so the risky primitive is not fully removed from the product surface. - The server-side method body is effectively unchanged in the provided diff. If the endpoint contributes to exploitability in older assets or alternate call paths, the patch does not conclusively eliminate that avenue.
- Because the remediation is centered in CP JavaScript, its protection scope depends on updated frontend assets being the only reachable path. That is weaker than a server-side invariant.
- The patch may be addressing an exploit chain component or enabling condition rather than the vulnerability class described by the CVE record, which creates residual uncertainty for defenders performing impact assessment.
Verdict
Partial fix.
Based strictly on the supplied sources, this patch reduces exposure by removing modern CP asset appends' reliance on the app/resource-js proxy and by making script loading deterministic. That is a meaningful mitigation if the exploit path depended on proxied script loading behavior. But the evidence does not show remediation of the Twig sandbox bypass itself, and the proxy endpoint remains present server-side in the provided patch context. For that reason, the change should be treated as an incomplete fix unless additional, unshown commits harden Twig sandboxing or revoke the underlying file-read primitive. Engineers should verify whether a separate server-side patch exists for template sandbox enforcement and whether legacy assets can still reach app/resource-js. Relevant references: GitHub patch, NVD, and CVE.org.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- Static Code Injection.php
Best fit for the Twig sandbox-bypass/theme-template aspect of CVE-2026-55792. This lab targets server-side code/template injection behavior in PHP and helps practice defensive fixes around untrusted template execution, dangerous evaluation paths, and restricting attacker-controlled rendering features.
- Path Traversal.py
Good conceptual match for the sensitive local file disclosure side of the CVE, where an attacker reads files such as .env. Even though the implementation language differs, the defensive lessons transfer well: constrain file access, normalize paths, enforce allowlists, and prevent attacker influence over file resolution.
- Path Traversal II.py
A stronger follow-on lab for defence-in-depth. It is useful after the first file-disclosure exercise because CVE-2026-55792 is not just about one unsafe call, but about enforcing boundaries so low-privileged users cannot escape intended content sandboxes and reach secrets on disk.