# Craft CMS: Blind SSRF and Arbitrary JavaScript Injection via Host Header Poisoning in actionResourceJs

Craft CMS actionResourceJs trusts poisoned Host/X-Forwarded-Host, letting an attacker proxy malicious JS as application/javascript, enabling cache-poisoned stored XSS and admin RCE.

- Payout: Research
- Severity: critical
- Program: Craft CMS
- Found by: seoyoung-kang
- Disclosed: 2026-06-19
- CVE: CVE-2026-55791
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-c55v-343g-5xff
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-c55v-343g-5xff
- Tags: area:web domain:web impact:ato impact:rce kind:research platform:ghsa sev:critical tech:php technique:chained vuln:cache-poisoning vuln:host-header vuln:rce vuln:ssrf vuln:xss

**Target:** /actions/app/resource-js endpoint (AppController::actionResourceJs)
**Root cause:** Default trustedHosts=['any'] lets X-Forwarded-Host poison $baseUrl, so the str_starts_with($url, $baseUrl) check passes and Guzzle (with ALLOW_REDIRECTS=true) fetches attacker content, which is reflected as application/javascript.
**Steps:**
1. Ensure assetManager.cacheSourcePaths is false (required condition).
2. Send request to `/actions/app/resource-js` with `X-Forwarded-Host: attacker.com` (or poisoned Host) so Yii2 sets $baseUrl to attacker domain.
3. Point the resource URL to attacker-controlled server; str_starts_with($url, $baseUrl) passes because $baseUrl is poisoned.
4. Craft::createGuzzleClient()->get($url) fetches attacker payload (redirects allowed).
5. Response returned via asRaw() with `Content-Type: application/javascript`.
6. If a caching layer exists, the malicious JS is cached for the legitimate URI and served to admins.
**How found:** Reporter analyzed the actionResourceJs URL validation and noticed $baseUrl derives from request headers; combined with default trustedHosts=['any'] and Guzzle ALLOW_REDIRECTS=true, the prefix check becomes attacker-controlled.
**Exploitation:** Unauthenticated attacker poisons Host/X-Forwarded-Host to make the endpoint proxy attacker JS. Behind a cache, the poisoned JS is stored for the resource-js URI; when an admin loads the Control Panel, the script reads window.Craft.csrfTokenValue and POSTs to /admin/actions/plugins/install-plugin, achieving 1-click RCE via session riding.
**Escalation:** SSRF chained with web cache poisoning to convert reflected JS into stored XSS against admins, then CSRF token theft to install a malicious plugin for RCE.
**Insight:** When a URL allowlist check compares against a value derived from request headers ($baseUrl), poisoning Host/X-Forwarded-Host inverts the check; also check whether the internal HTTP client follows redirects, since that reopens SSRF past prefix validation.

