CVE Patch Review

CVE-2026-55792 Patch Review: Partial Fix for Craft CMS Sandbox-Bypass File Disclosure

CVE-2026-55792 · Updated 2026-07-07 Partial fix

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, '&amp;'));
    }
    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-js endpoint 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.

Sources