CVE-2024-25737 (published 2024-05-22T19:15Z)

A Server-Side Request Forgery (SSRF) vulnerability in the /Cover/Show route (showAction in CoverController.php) in Open Library Foundation VuFind 2.4 through 9.1 before 9.1.1 allows remote attackers to access internal HTTP servers and perform Cross-Site Scripting (XSS) attacks by proxying arbitrary URLs via the proxy GET parameter.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/vufind-org/vufind
commit_details: repo_url: https://github.com/vufind-org/vufind
author: Demian Katz
author_date: 2024-02-06 08:13:43-05:00
msg: Restrict hostnames for cover proxy. (#3385)
repository: repo_url: https://github.com/vufind-org/vufind
repo_name: vufind-org/vufind
description: A library resource discovery portal designed and developed for libraries by libraries
repo_language: PHP
owner: vufind-org
file_changes: filename: config.ini
change_type: ModificationType.MODIFY
diff: @@ -1023,6 +1023,13 @@ coverimagesCache = true
 ; BrowZine DOI icons:
 coverproxyCache[] = "/assets\.thirdiron\.com/"
 
+; This setting controls which hosts are allowed to provide cover images through
+; the built-in cover proxy. Images from hostnames that do not match the regular
+; expressions below will not be proxied. Be sure to define regular expressions
+; using an end of string ($) marker to prevent abuse.
+coverproxyAllowedHosts[] = "/assets\.thirdiron\.com$/"
+coverproxyAllowedHosts[] = "/\.summon\.serialssolutions\.com$/"
+
 ; This setting controls how cover image URLs are loaded. They could be loaded as
 ; part of main request, or asynchronously. Asynchronous loading is disabled by
 ; default; to enable it, just uncomment the line below.

programming_language: Markdown
filename: CoverController.php
change_type: ModificationType.MODIFY
diff: @@ -65,21 +65,31 @@ class CoverController extends \Laminas\Mvc\Controller\AbstractActionController
      */
     protected $sessionSettings = null;
 
+    /**
+     * Configuration settings ([Content] section of config.ini)
+     *
+     * @var array
+     */
+    protected $config;
+
     /**
      * Constructor
      *
      * @param Loader          $loader Cover loader
      * @param CachingProxy    $proxy  Proxy loader
      * @param SessionSettings $ss     Session settings
+     * @param array           $config Configuration settings
      */
     public function __construct(
         Loader $loader,
         CachingProxy $proxy,
-        SessionSettings $ss
+        SessionSettings $ss,
+        array $config = []
     ) {
         $this->loader = $loader;
         $this->proxy = $proxy;
         $this->sessionSettings = $ss;
+        $this->config = $config;
     }
 
     /**
@@ -115,6 +125,27 @@ class CoverController extends \Laminas\Mvc\Controller\AbstractActionController
         ];
     }
 
+    /**
+     * Is the provided URL included on the configured allow list?
+     *
+     * @param string $url URL to check
+     *
+     * @return bool
+     */
+    protected function proxyAllowedForUrl(string $url): bool
+    {
+        $host = parse_url($url, PHP_URL_HOST);
+        if (!$host) {
+            return false;
+        }
+        foreach ((array)($this->config['coverproxyAllowedHosts'] ?? []) as $regEx) {
+            if (preg_match($regEx, $host)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Send image data for display in the view
      *
@@ -126,7 +157,7 @@ class CoverController extends \Laminas\Mvc\Controller\AbstractActionController
 
         // Special case: proxy a full URL:
         $url = $this->params()->fromQuery('proxy');
-        if (!empty($url)) {
+        if (!empty($url) && $this->proxyAllowedForUrl($url)) {
             try {
                 $image = $this->proxy->fetch($url);
                 $contentType = $image?->getHeaders()?->get('content-type')?->getFieldValue() ?? '';

programming_language: PHP
filename: CoverControllerFactory.php
change_type: ModificationType.MODIFY
diff: @@ -68,10 +68,13 @@ class CoverControllerFactory implements FactoryInterface
         if (!empty($options)) {
             throw new \Exception('Unexpected options sent to factory.');
         }
+        $config = $container->get(\VuFind\Config\PluginManager::class)->get('config');
+        $configArray = $config?->Content?->toArray() ?? [];
         return new $requestedName(
             $container->get(\VuFind\Cover\Loader::class),
             $container->get(\VuFind\Cover\CachingProxy::class),
-            $container->get(\VuFind\Session\Settings::class)
+            $container->get(\VuFind\Session\Settings::class),
+            $configArray
         );
     }
 }

programming_language: PHP
repo_url: https://github.com/vufind-org/vufind
commit_details: repo_url: https://github.com/vufind-org/vufind
author: Demian Katz
author_date: 2024-02-06 15:08:29-05:00
msg: Make cover proxy legal content types configurable. (#3388)
repository: repo_url: https://github.com/vufind-org/vufind
repo_name: vufind-org/vufind
description: A library resource discovery portal designed and developed for libraries by libraries
repo_language: PHP
owner: vufind-org
file_changes: filename: config.ini
change_type: ModificationType.MODIFY
diff: @@ -1030,6 +1030,13 @@ coverproxyCache[] = "/assets\.thirdiron\.com/"
 coverproxyAllowedHosts[] = "/assets\.thirdiron\.com$/"
 coverproxyAllowedHosts[] = "/\.summon\.serialssolutions\.com$/"
 
+; This setting controls the content types allowed through the cover proxy.
+; Note that proxying SVG files is not recommended because of their potential for
+; abuse in cross-site scripting attacks.
+coverproxyAllowedTypes[] = "image/gif"
+coverproxyAllowedTypes[] = "image/jpeg"
+coverproxyAllowedTypes[] = "image/png"
+
 ; This setting controls how cover image URLs are loaded. They could be loaded as
 ; part of main request, or asynchronously. Asynchronous loading is disabled by
 ; default; to enable it, just uncomment the line below.

programming_language: Markdown
filename: CoverController.php
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,8 @@ use VuFind\Cover\CachingProxy;
 use VuFind\Cover\Loader;
 use VuFind\Session\Settings as SessionSettings;
 
+use function in_array;
+
 /**
  * Generates covers for book entries
  *
@@ -146,6 +148,20 @@ class CoverController extends \Laminas\Mvc\Controller\AbstractActionController
         return false;
     }
 
+    /**
+     * Is the content type allowed by the cover proxy?
+     *
+     * @param string $contentType Type to check
+     *
+     * @return bool
+     */
+    protected function isValidProxyImageContentType(string $contentType): bool
+    {
+        $validTypes = $this->config['coverproxyAllowedTypes']
+            ?? ['image/gif', 'image/jpeg', 'image/png'];
+        return in_array(strtolower($contentType), array_map('strtolower', $validTypes));
+    }
+
     /**
      * Send image data for display in the view
      *
@@ -161,7 +177,7 @@ class CoverController extends \Laminas\Mvc\Controller\AbstractActionController
             try {
                 $image = $this->proxy->fetch($url);
                 $contentType = $image?->getHeaders()?->get('content-type')?->getFieldValue() ?? '';
-                if (str_starts_with(strtolower($contentType), 'image/')) {
+                if ($this->isValidProxyImageContentType($contentType)) {
                     return $this->displayImage(
                         $contentType,
                         $image->getContent()

programming_language: PHP