VYPR
Medium severityNVD Advisory· Published Apr 22, 2026· Updated Apr 22, 2026

CVE-2026-41130

CVE-2026-41130

Description

Craft CMS is a content management system (CMS). In versions on the 4.x branch through 4.17.8 and the 5.x branch through 5.9.14, the resource-js endpoint in Craft CMS allows unauthenticated requests to proxy remote JavaScript resources. When trustedHosts is not explicitly restricted (default configuration), the application trusts the client-supplied Host header. This allows an attacker to control the derived baseUrl, which is used in prefix validation inside actionResourceJs(). By supplying a malicious Host header, the attacker can make the server issue arbitrary HTTP requests, leading to Server-Side Request Forgery (SSRF). Versions 4.17.9 and 5.9.15 patch the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
craftcms/cmsPackagist
>= 5.0.0-RC1, < 5.9.155.9.15
craftcms/cmsPackagist
>= 4.0.0-RC1, < 4.17.94.17.9

Affected products

1

Patches

1
ebe7e85f1c89

Fixed GHSA-95wr-3f2v-v2wh

https://github.com/craftcms/cmsbrandonkellyMar 5, 2026via ghsa
4 files changed · +89 42
  • CHANGELOG.md+2 1 modified
    @@ -2,13 +2,14 @@
     
     ## Unreleased
     
    +- Added `craft\helpers\App::resourcePathByUri()`.
     - Fixed a bug where global set GraphQL query caches weren’t getting invalidated when global sets were updated. ([#18479](https://github.com/craftcms/cms/issues/18479))
     - Fixed a bug where `users/suspend-user` and `users/unsuspend-user` actions required that the logged-in user have control panel access. ([#18485](https://github.com/craftcms/cms/issues/18485))
     - Fixed a bug where flipping an image within the Image Editor didn’t always work. ([#18486](https://github.com/craftcms/cms/issues/18486))
     - Fixed a bug where SVG files missing their `width` and `height` attributes weren’t getting them set as expected.
     - Fixed an error that occurred if a template referenced a preloaded Single entry followed by a null coalescing operator. ([#18503](https://github.com/craftcms/cms/issues/18503))
     - Fixed a bug where links within Redactor fields were getting `target="_blank"` added to them. ([#18500](https://github.com/craftcms/cms/issues/18500))
    -- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSRF vulnerability. (GHSA-3m9m-24vh-39wx)
    +- Fixed [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSRF vulnerabilities. (GHSA-3m9m-24vh-39wx, GHSA-95wr-3f2v-v2wh)
     
     ## 4.17.8 - 2026-02-25
     
    
  • src/controllers/AppController.php+15 7 modified
    @@ -23,6 +23,7 @@
     use craft\helpers\Html;
     use craft\helpers\Json;
     use craft\helpers\Session;
    +use craft\helpers\StringHelper;
     use craft\helpers\Update as UpdateHelper;
     use craft\helpers\UrlHelper;
     use craft\models\Update;
    @@ -32,6 +33,7 @@
     use craft\web\ServiceUnavailableHttpException;
     use DateInterval;
     use Throwable;
    +use yii\base\InvalidArgumentException;
     use yii\base\InvalidConfigException;
     use yii\web\BadRequestHttpException;
     use yii\web\Cookie;
    @@ -110,17 +112,23 @@ public function actionResourceJs(string $url): Response
         {
             $this->requireCpRequest();
     
    -        if (!str_starts_with($url, Craft::$app->getAssetManager()->baseUrl)) {
    +        $assetManager = Craft::$app->getAssetManager();
    +        $baseUrl = StringHelper::ensureRight($assetManager->baseUrl, '/');
    +        if (!str_starts_with($url, $baseUrl)) {
                 throw new BadRequestHttpException("$url does not appear to be a resource URL");
             }
     
    -        // Close the PHP session in case this takes a while
    -        Session::close();
    +        $resourceUri = preg_replace('/^(.*)\?.*/', '$1', substr($url, strlen($baseUrl)));
     
    -        $response = Craft::createGuzzleClient()->get($url);
    -        $this->response->setCacheHeaders();
    -        $this->response->getHeaders()->set('content-type', 'application/javascript');
    -        return $this->asRaw($response->getBody());
    +        try {
    +            $publishedPath = App::resourcePathByUri($resourceUri);
    +        } catch (InvalidArgumentException $e) {
    +            throw new BadRequestHttpException($e->getMessage(), previous: $e);
    +        }
    +
    +        return $this->response->sendFile($publishedPath, null, [
    +            'inline' => true,
    +        ]);
         }
     
         /**
    
  • src/helpers/App.php+68 0 modified
    @@ -15,6 +15,8 @@
     use craft\db\Connection;
     use craft\db\mysql\Schema as MysqlSchema;
     use craft\db\pgsql\Schema as PgsqlSchema;
    +use craft\db\Query;
    +use craft\db\Table;
     use craft\elements\User;
     use craft\enums\LicenseKeyStatus;
     use craft\errors\InvalidPluginException;
    @@ -41,6 +43,7 @@
     use yii\base\Exception;
     use yii\base\InvalidArgumentException;
     use yii\base\InvalidValueException;
    +use yii\db\Exception as DbException;
     use yii\helpers\Inflector;
     use yii\mutex\FileMutex;
     use yii\mutex\MysqlMutex;
    @@ -1483,4 +1486,69 @@ public static function configure(object $object, array $properties): void
                 $object->$name = $value;
             }
         }
    +
    +    /**
    +     * Returns the path for a CP resource by its URI.
    +     *
    +     * @param string $uri
    +     * @return string|null
    +     * @throws InvalidArgumentException
    +     * @since 4.17.9
    +     */
    +    public static function resourcePathByUri(string $uri): ?string
    +    {
    +        if (!Path::ensurePathIsContained($uri)) {
    +            throw new InvalidArgumentException("Invalid resource: $uri");
    +        }
    +
    +        $assetManager = Craft::$app->getAssetManager();
    +
    +        // If the file already exists, return that
    +        $path = "$assetManager->basePath/$uri";
    +        if (file_exists($path)) {
    +            return $path;
    +        }
    +
    +        // Otherwise, publish it
    +        $slash = strpos($uri, '/');
    +        $hash = substr($uri, 0, $slash);
    +        $sourcePath = self::resourceSourcePathByHash($hash, $assetManager);
    +
    +        if (!$sourcePath) {
    +            return null;
    +        }
    +
    +        $filePath = substr($uri, strlen($hash) + 1);
    +
    +        // Publish the directory
    +        [$publishedDir] = $assetManager->publish(Craft::getAlias($sourcePath));
    +
    +        $publishedPath = $publishedDir . DIRECTORY_SEPARATOR . $filePath;
    +        if (!file_exists($publishedPath)) {
    +            throw new InvalidArgumentException("$filePath does not exist.");
    +        }
    +
    +        return $publishedPath;
    +    }
    +
    +    /**
    +     * Returns the source path for a CP resource by its hash.
    +     *
    +     * @param string $hash
    +     * @return string|false
    +     * @since 4.17.9
    +     */
    +    private static function resourceSourcePathByHash(string $hash, AssetManager $assetManager): string|false
    +    {
    +        try {
    +            return (new Query())
    +                ->select(['path'])
    +                ->from(Table::RESOURCEPATHS)
    +                ->where(['hash' => $hash])
    +                ->scalar();
    +        } catch (DbException) {
    +            // Craft isn't installed yet. See if it's cached as a fallback.
    +            return Craft::$app->getCache()->get($assetManager->getCacheKeyForPathHash($hash));
    +        }
    +    }
     }
    
  • src/web/Application.php+4 34 modified
    @@ -9,7 +9,6 @@
     
     use Craft;
     use craft\base\ApplicationTrait;
    -use craft\db\Query;
     use craft\db\Table;
     use craft\debug\DeprecatedPanel;
     use craft\debug\DumpPanel;
    @@ -38,7 +37,6 @@
     use yii\base\InvalidArgumentException;
     use yii\base\InvalidConfigException;
     use yii\base\InvalidRouteException;
    -use yii\db\Exception as DbException;
     use yii\debug\Module as YiiDebugModule;
     use yii\debug\panels\AssetPanel;
     use yii\debug\panels\DbPanel;
    @@ -523,25 +521,11 @@ private function _processResourceRequest(): void
             }
     
             $resourceUri = substr($requestPath, strlen($resourceBaseUri));
    -        $slash = strpos($resourceUri, '/');
    -        $hash = substr($resourceUri, 0, $slash);
    -        $sourcePath = $this->resourceSourcePathByHash($hash);
     
    -        if (!$sourcePath) {
    -            return;
    -        }
    -
    -        $filePath = substr($resourceUri, strlen($hash) + 1);
    -        if (!Path::ensurePathIsContained($filePath)) {
    -            throw new BadRequestHttpException('Invalid resource path: ' . $filePath);
    -        }
    -
    -        // Publish the directory
    -        [$publishedDir] = $this->getAssetManager()->publish(Craft::getAlias($sourcePath));
    -
    -        $publishedPath = $publishedDir . DIRECTORY_SEPARATOR . $filePath;
    -        if (!file_exists($publishedPath)) {
    -            throw new NotFoundHttpException("$filePath does not exist.");
    +        try {
    +            $publishedPath = App::resourcePathByUri($resourceUri);
    +        } catch (InvalidArgumentException $e) {
    +            throw new BadRequestHttpException($e->getMessage(), previous: $e);
             }
     
             $response = $this->getResponse();
    @@ -558,20 +542,6 @@ private function _processResourceRequest(): void
             $this->end();
         }
     
    -    private function resourceSourcePathByHash(string $hash): string|false
    -    {
    -        try {
    -            return (new Query())
    -                ->select(['path'])
    -                ->from(Table::RESOURCEPATHS)
    -                ->where(['hash' => $hash])
    -                ->scalar();
    -        } catch (DbException) {
    -            // Craft isn't installed yet. See if it's cached as a fallback.
    -            return Craft::$app->getCache()->get(Craft::$app->getAssetManager()->getCacheKeyForPathHash($hash));
    -        }
    -    }
    -
         /**
          * Processes install requests.
          *
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.