CVE-2024-29023 (published 2024-04-12T21:15Z)

Xibo is an Open Source Digital Signage platform with a web content management system and Windows display player software. Session tokens are exposed in the return of session search API call on the sessions page. Subsequently they can be exfiltrated and used to hijack a session. Users must be granted access to the session page, or be a super admin. Users should upgrade to version 3.3.10 or 4.0.9 which fix this issue. Customers who host their CMS with the Xibo Signage service have already received an upgrade or patch to resolve this issue regardless of the CMS version that they are running. Patches are available for earlier versions of Xibo CMS that are out of security support: 2.3 patch ebeccd000b51f00b9a25f56a2f252d6812ebf850.diff. 1.8 patch a81044e6ccdd92cc967e34c125bd8162432e51bc.diff. There are no known workarounds for this vulnerability.


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/dasgarner/xibo-cms
commit_details: repo_url: https://github.com/dasgarner/xibo-cms
author: Dan Garner
author_date: 2024-03-18 08:52:12+00:00
msg: Sessions: Add sanitization in getIp() and disable output of session ID in session grid.
repository: repo_url: https://github.com/dasgarner/xibo-cms
repo_name: dasgarner/xibo-cms
description: Xibo Content Management System
repo_language: PHP
owner: dasgarner
file_changes: filename: Sessions.php
change_type: ModificationType.MODIFY
diff: @@ -88,11 +88,11 @@ class Sessions extends Base
                 $row->includeProperty('buttons');
 
                 // Edit
-                $row->buttons[] = array(
+                /*$row->buttons[] = array(
                     'id' => 'sessions_button_logout',
                     'url' => $this->urlFor('sessions.confirm.logout.form', ['id' => $row->sessionId]),
                     'text' => __('Logout')
-                );
+                );*/
             }
         }
 

programming_language: PHP
filename: SessionFactory.php
change_type: ModificationType.MODIFY
diff: @@ -150,7 +150,14 @@ class SessionFactory extends BaseFactory
 
 
         foreach ($this->getStore()->select($sql, $params) as $row) {
-            $entries[] = $this->createEmpty()->hydrate($row, ['stringProperties' => ['sessionId']]);
+            $session = $this->createEmpty()->hydrate($row, [
+                'stringProperties' => ['sessionId'],
+                'intProperties' => ['isExpired']
+            ]);
+            $session->userAgent = htmlspecialchars($session->userAgent);
+            $session->remoteAddress = filter_var($session->remoteAddress, FILTER_VALIDATE_IP);
+            $session->excludeProperty('sessionId');
+            $entries[] = $session;
         }
 
         // Paging

programming_language: PHP
filename: Session.php
change_type: ModificationType.MODIFY
diff: @@ -170,7 +170,7 @@ class Session implements \SessionHandlerInterface
         $data = '';
         $this->key = $key;
 
-        $userAgent = substr($_SERVER['HTTP_USER_AGENT'], 0, 253);
+        $userAgent = substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253);
 
         try {
             $dbh = $this->getDb();
@@ -420,7 +420,7 @@ class Session implements \SessionHandlerInterface
             'lastAccessed' => date('Y-m-d H:i:s', $lastAccessed),
             'userId' => $this->userId,
             'expired' => ($this->expired) ? 1 : 0,
-            'useragent' => substr($_SERVER['HTTP_USER_AGENT'], 0, 253),
+            'useragent' => substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253),
             'remoteaddr' => $this->getIp()
         ];
 
@@ -469,7 +469,7 @@ class Session implements \SessionHandlerInterface
         $clientIp = '';
         $keys = array('X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP', 'REMOTE_ADDR');
         foreach ($keys as $key) {
-            if (isset($_SERVER[$key])) {
+            if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP) !== false) {
                 $clientIp = $_SERVER[$key];
                 break;
             }

programming_language: PHP
filename: Soap.php
change_type: ModificationType.MODIFY
diff: @@ -1772,7 +1772,7 @@ class Soap
 
         $keys = array('X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP', 'REMOTE_ADDR');
         foreach ($keys as $key) {
-            if (isset($_SERVER[$key])) {
+            if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP) !== false) {
                 $clientIp = $_SERVER[$key];
                 break;
             }

programming_language: PHP
repo_url: https://github.com/xibosignage/xibo-cms
commit_details: repo_url: https://github.com/xibosignage/xibo-cms
author: Dan Garner
author_date: 2024-03-18 12:40:49+00:00
msg: Improvements to filtering IP and sessions (#2433)

* Sessions: Add sanitization in getIp() and disable output of session ID in session grid. xibosignage/xibo#3375
* Bump to 4.0.9
repository: repo_url: https://github.com/xibosignage/xibo-cms
repo_name: xibosignage/xibo-cms
description: Xibo Content Management System
repo_language: PHP
owner: xibosignage
file_changes: filename: Sessions.php
change_type: ModificationType.MODIFY
diff: @@ -96,6 +96,11 @@ class Sessions extends Base
             if (!$this->isApi($request) && $this->getUser()->isSuperAdmin()) {
                 $row->includeProperty('buttons');
 
+                // No buttons on expired sessions
+                if ($row->isExpired == 1) {
+                    continue;
+                }
+
                 // logout, current user/session
                 if ($row->userId === $this->getUser()->userId && session_id() === $row->sessionId) {
                     $url = $this->urlFor($request, 'logout');
@@ -104,7 +109,7 @@ class Sessions extends Base
                     $url = $this->urlFor(
                         $request,
                         'sessions.confirm.logout.form',
-                        ['id' => $row->sessionId]
+                        ['id' => $row->userId]
                     );
                 }
 
@@ -127,13 +132,13 @@ class Sessions extends Base
      * Confirm Logout Form
      * @param Request $request
      * @param Response $response
-     * @param $id
+     * @param int $id The UserID
      * @return \Psr\Http\Message\ResponseInterface|Response
      * @throws AccessDeniedException
      * @throws \Xibo\Support\Exception\ControllerNotImplemented
      * @throws \Xibo\Support\Exception\GeneralException
      */
-    function confirmLogoutForm(Request $request, Response $response, $id)
+    public function confirmLogoutForm(Request $request, Response $response, $id)
     {
         if ($this->getUser()->userTypeId != 1) {
             throw new AccessDeniedException();
@@ -141,7 +146,7 @@ class Sessions extends Base
 
         $this->getState()->template = 'sessions-form-confirm-logout';
         $this->getState()->setData([
-            'sessionId' => $id,
+            'userId' => $id,
         ]);
 
         return $this->render($request, $response);
@@ -158,25 +163,14 @@ class Sessions extends Base
      * @throws \Xibo\Support\Exception\GeneralException
      * @throws \Xibo\Support\Exception\NotFoundException
      */
-    function logout(Request $request, Response $response, $id)
+    public function logout(Request $request, Response $response, $id)
     {
         if ($this->getUser()->userTypeId != 1) {
             throw new AccessDeniedException();
         }
 
-        $session = $this->sessionFactory->getById($id);
-
-        if ($session->userId != 0) {
-            $this->store->update(
-                'UPDATE `session` SET IsExpired = 1 WHERE userID = :userId ',
-                ['userId' => $session->userId]
-            );
-        } else {
-            $this->store->update(
-                'UPDATE `session` SET IsExpired = 1 WHERE session_id = :session_id ',
-                ['session_id' => $id]
-            );
-        }
+        // We log out all of this user's sessions.
+        $this->sessionFactory->expireByUserId($id);
 
         // Return
         $this->getState()->hydrate([

programming_language: PHP
filename: SessionFactory.php
change_type: ModificationType.MODIFY
diff: @@ -1,8 +1,8 @@
 <?php
 /*
- * Copyright (c) 2022 Xibo Signage Ltd
+ * Copyright (C) 2024 Xibo Signage Ltd
  *
- * Xibo - Digital Signage - http://www.xibo.org.uk
+ * Xibo - Digital Signage - https://xibosignage.com
  *
  * This file is part of Xibo.
  *
@@ -26,7 +26,6 @@ namespace Xibo\Factory;
 
 use Xibo\Entity\Session;
 use Xibo\Helper\DateFormatHelper;
-use Xibo\Support\Exception\NotFoundException;
 
 /**
  * Class SessionFactory
@@ -43,18 +42,14 @@ class SessionFactory extends BaseFactory
     }
 
     /**
-     * @param $sessionId
-     * @return Session
-     * @throws NotFoundException
+     * @param int $userId
      */
-    public function getById($sessionId)
+    public function expireByUserId(int $userId): void
     {
-        $session = $this->query(null, ['sessionId' => $sessionId]);
-
-        if (count($session) <= 0)
-            throw new NotFoundException();
-
-        return $session[0];
+        $this->getStore()->update(
+            'UPDATE `session` SET IsExpired = 1 WHERE userID = :userId ',
+            ['userId' => $userId]
+        );
     }
 
     /**
@@ -143,7 +138,14 @@ class SessionFactory extends BaseFactory
 
 
         foreach ($this->getStore()->select($sql, $params) as $row) {
-            $entries[] = $this->createEmpty()->hydrate($row, ['stringProperties' => ['sessionId']]);
+            $session = $this->createEmpty()->hydrate($row, [
+                'stringProperties' => ['sessionId'],
+                'intProperties' => ['isExpired'],
+            ]);
+            $session->userAgent = htmlspecialchars($session->userAgent);
+            $session->remoteAddress = filter_var($session->remoteAddress, FILTER_VALIDATE_IP);
+            $session->excludeProperty('sessionId');
+            $entries[] = $session;
         }
 
         // Paging

programming_language: PHP
filename: Environment.php
change_type: ModificationType.MODIFY
diff: @@ -30,7 +30,7 @@ use Phinx\Wrapper\TextWrapper;
  */
 class Environment
 {
-    public static $WEBSITE_VERSION_NAME = '4.0.8';
+    public static $WEBSITE_VERSION_NAME = '4.0.9';
     public static $XMDS_VERSION = '7';
     public static $XLF_VERSION = 4;
     public static $VERSION_REQUIRED = '8.1.0';

programming_language: PHP
filename: Session.php
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 <?php
 /*
- * Copyright (C) 2023 Xibo Signage Ltd
+ * Copyright (C) 2024 Xibo Signage Ltd
  *
  * Xibo - Digital Signage - https://xibosignage.com
  *
@@ -172,7 +172,7 @@ class Session implements \SessionHandlerInterface
         $data = '';
         $this->key = $key;
 
-        $userAgent = substr($_SERVER['HTTP_USER_AGENT'], 0, 253);
+        $userAgent = substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253);
 
         try {
             $dbh = $this->getDb();
@@ -422,7 +422,7 @@ class Session implements \SessionHandlerInterface
             'lastAccessed' => Carbon::createFromTimestamp($lastAccessed)->format(DateFormatHelper::getSystemFormat()),
             'userId' => $this->userId,
             'expired' => ($this->expired) ? 1 : 0,
-            'useragent' => substr($_SERVER['HTTP_USER_AGENT'], 0, 253),
+            'useragent' => substr(htmlspecialchars($_SERVER['HTTP_USER_AGENT']), 0, 253),
             'remoteaddr' => $this->getIp()
         ];
 
@@ -471,7 +471,7 @@ class Session implements \SessionHandlerInterface
         $clientIp = '';
         $keys = array('X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP', 'REMOTE_ADDR');
         foreach ($keys as $key) {
-            if (isset($_SERVER[$key])) {
+            if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP) !== false) {
                 $clientIp = $_SERVER[$key];
                 break;
             }

programming_language: PHP
filename: Soap.php
change_type: ModificationType.MODIFY
diff: @@ -2640,7 +2640,7 @@ class Soap
 
         $keys = array('X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP', 'REMOTE_ADDR');
         foreach ($keys as $key) {
-            if (isset($_SERVER[$key])) {
+            if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP) !== false) {
                 $clientIp = $_SERVER[$key];
                 break;
             }

programming_language: PHP
filename: sessions-form-confirm-logout.twig
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,7 @@
 {% block formHtml %}
     <div class="row">
         <div class="col-md-12">
-            <form id="sessionsConfirmLogoutForm" class="XiboForm form-horizontal" method="delete" action="{{ url_for("sessions.confirm.logout", {id: sessionId}) }}">
+            <form id="sessionsConfirmLogoutForm" class="XiboForm form-horizontal" method="delete" action="{{ url_for("sessions.confirm.logout", {id: userId}) }}">
                 {% set message %}{% trans "Are you sure you want to logout this user?" %}{% endset %}
                 {{ forms.message(message) }}
             </form>

programming_language: HTML