Craft CMS: Blind SSRF and Arbitrary JavaScript Injection via Host Header Poisoning in actionResourceJs
Description
1. Overview
Craft CMS is vulnerable to Server-Side Request Forgery (SSRF) and Arbitrary JavaScript Injection through the /actions/app/resource-js endpoint. By exploiting the default permissive trustedHosts configuration, an attacker can poison the Host or X-Forwarded-Host header to manipulate the application’s $baseUrl. This bypasses the endpoint’s internal URL validation, forcing the backend Guzzle client to fetch a malicious payload from an attacker-controlled server and reflect it to the client with a Content-Type: application/javascript header.
2. Vulnerability Mechanism (Root Cause) The vulnerability manifests when assetManager.cacheSourcePaths is set to false. The attack chain relies on three structural flaws and insecure defaults:
- **A. Default Proxy Trust (
trustedHosts):** Craft’s defaultGeneralConfig::$trustedHostsis set to['any']. This allows an attacker to bypass front-end web server (Nginx/Apache) strictHostheader validations by simply injecting anX-Forwarded-Hostheader. Yii2 will parse this and globally set$baseUrlto the attacker's domain. - **B. Insecure HTTP Client (
actionResourceJs):** InAppController::actionResourceJs(), thestr_starts_with($url, $baseUrl)validation is bypassed because$baseUrlis already poisoned by the attacker. The core then usesCraft::createGuzzleClient()->get($url). Unlike the GraphQL Asset fetcher, this Guzzle instance defaults toALLOW_REDIRECTS => true. - C. Forced JS Content-Type: The response fetched from the attacker's server is blindly returned to the user via
$this->asRaw()with the headerContent-Type: application/javascript.
3. Attack Scenario & Impact (Proof of Exploitability) This endpoint acts as a proxy, taking remote, unverified content and serving it as valid JavaScript. While the direct SSRF allows for internal network probing, the most devastating impact occurs when caching layers are involved.
If the Craft CMS instance is behind a caching layer, this vulnerability leads directly to Web Cache Poisoning:
- An unauthenticated attacker sends the poisoned request.
- The caching layer caches the malicious JavaScript response for the legitimate
/actions/app/resource-jsURI. - When an authenticated Administrator logs into the Control Panel, their browser loads the poisoned cached JavaScript (Stored XSS).
- The malicious script extracts
window.Craft.csrfTokenValueand silently sends a POST request to/admin/actions/plugins/install-plugin, achieving 1-Click Remote Code Execution (RCE) via Session Riding.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Affected products
1Patches
Vulnerability mechanics
Root cause
"The `actionResourceJs()` endpoint fetches arbitrary URLs via Guzzle and returns the response as JavaScript, with Host-header validation bypassable due to the default permissive `trustedHosts` configuration."
Attack vector
An unauthenticated attacker sends a request to `/actions/app/resource-js` with a crafted `url` parameter pointing to an attacker-controlled server. Because Craft's default `trustedHosts` configuration is set to `['any']`, the attacker can poison the `Host` or `X-Forwarded-Host` header to manipulate `$baseUrl`, bypassing the `str_starts_with($url, $baseUrl)` check. The backend Guzzle client then fetches the attacker's payload and reflects it with `Content-Type: application/javascript`. When a caching layer is present, this response can be cached and served to an authenticated administrator, leading to stored XSS and session-riding RCE. [ref_id=1]
Affected code
The vulnerability resides in `src/controllers/AppController.php` in the `actionResourceJs()` method, which is entirely removed by the patch. The method used `Craft::createGuzzleClient()->get($url)` to fetch arbitrary JavaScript resources and returned them with `Content-Type: application/javascript`. The patch also rewrites `src/web/assets/cp/src/js/Craft.js` to replace the jQuery-based `_appendHtml()` with native sequential script loading, removing the need for the proxy endpoint entirely.
What the fix does
The patch removes the entire `actionResourceJs()` method from `AppController.php`, eliminating the SSRF/JS-injection vector at its source. In `Craft.js`, the `_appendHtml()` function is rewritten to load scripts sequentially via native `<script>` element insertion with explicit `await` on `onload`/`onerror` events, and a `_queueAppendHtml()` method ensures that non-awaited call sites still execute in order. This removes the need for the proxy endpoint because cross-domain scripts are now loaded directly by the browser rather than proxied through the server. [patch_id=6633383]
Preconditions
- configThe `assetManager.cacheSourcePaths` configuration must be set to `false`.
- configThe default `trustedHosts` configuration must be set to `['any']` (the default).
- networkThe attacker must be able to send HTTP requests to the Craft CMS instance.
Generated on Jun 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.