CVE-2023-3520: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute (CWE-614)
value: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute in GitHub repository it-novum/openitcockpit prior to 4.6.6.
CWE description: The Secure attribute for sensitive cookies in HTTPS sessions is not set, which could cause the user agent to send those cookies in plaintext over an HTTP session.
CVSS v3 base score: 4.6
Published: 2023-07-06T01:15Z
Repository: https://github.com/it-novum/openitcockpit
Language: PHP
Files changed: config/routes.php, src/Application.php, src/Controller/StatisticsController.php, webroot/js/scripts/controllers/Administrator/AdministratorsDebugController.js
Commit date: 2023-06-15 10:03:44 +0200
Commit message: ITC-3039 Set all cookies with secure=true. Many thanks to https://github.com/uonghoangminhchau for reporting this

Diff: diff --git a/config/routes.php b/config/routes.php
index 3adff010f..431a2968b 100644
--- a/config/routes.php
+++ b/config/routes.php
@@ -54,10 +54,11 @@ $routes->setExtensions(['json', 'html', 'pdf', 'png', 'zip']);
 
 $routes->scope('/', function (RouteBuilder $builder) {
     // Register scoped middleware for in scopes.
     $csrf = new CsrfProtectionMiddleware([
         'httponly' => true,
+        'secure'   => true
     ]);
 
     // Token check will be skipped when callback returns `true`.
     $csrf->skipCheckCallback(function ($request) {
         // Skip token check for API URLs.
diff --git a/src/Application.php b/src/Application.php
index 30beeda49..b0dde6685 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -109,11 +109,12 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
      * @return void
      */
     public function routes($routes): void {
         // Register scoped middleware for use in routes.php
         $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
-            'httponly' => true
+            'httponly' => true,
+            'secure'   => true
         ]));
 
         parent::routes($routes);
     }
 
@@ -187,11 +188,13 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
         $expireAt->setTimestamp(time() + (3600 * 24 * 31)); // In one month
         $service->loadAuthenticator('Authentication.Cookie', [
             'rememberMeField' => 'remember_me',
             'fields'          => $fields,
             'cookie'          => [
-                'expires' => $expireAt
+                'expires'  => $expireAt,
+                'httponly' => true,
+                'secure'   => true
             ]
         ]);
         $service->loadAuthenticator('Authentication.Session');
         $service->loadAuthenticator('Authentication.Form', [
             'fields'   => $fields,
diff --git a/src/Controller/StatisticsController.php b/src/Controller/StatisticsController.php
index 72f5adf37..062cedbe7 100644
--- a/src/Controller/StatisticsController.php
+++ b/src/Controller/StatisticsController.php
@@ -96,11 +96,13 @@ class StatisticsController extends AppController {
         if ($this->request->getData('statistics.cookie', null) !== null && $record->get('value') === 2) {
             $this->response = $this->response->withCookie(new Cookie(
                 'askAgainForHelp',
                 'Remind me later',
                 new \DateTime('+16 hours'),
-                '/'
+                '/',
+                null,
+                true
             ));
         }
 
         $SystemsettingsTable->save($record);
         if ($record->hasErrors()) {
diff --git a/src/itnovum/openITCOCKPIT/Core/Security/CSRF.php b/src/itnovum/openITCOCKPIT/Core/Security/CSRF.php
deleted file mode 100644
index 81934249c..000000000
--- a/src/itnovum/openITCOCKPIT/Core/Security/CSRF.php
+++ /dev/null
@@ -1,179 +0,0 @@
-<?php
-// Copyright (C) <2018>  <it-novum GmbH>
-//
-// This file is dual licensed
-//
-// 1.
-//  This program is free software: you can redistribute it and/or modify
-//  it under the terms of the GNU General Public License as published by
-//  the Free Software Foundation, version 3 of the License.
-//
-//  This program is distributed in the hope that it will be useful,
-//  but WITHOUT ANY WARRANTY; without even the implied warranty of
-//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-//  GNU General Public License for more details.
-//
-//  You should have received a copy of the GNU General Public License
-//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
-//
-// 2.
-//  If you purchased an openITCOCKPIT Enterprise Edition you can use this file
-//  under the terms of the openITCOCKPIT Enterprise Edition license agreement.
-//  License agreement and license key will be shipped with the order
-//  confirmation.
-
-namespace itnovum\openITCOCKPIT\Core\Security;
-
-
-class CSRF {
-
-    /**
-     * @var \SessionComponent
-     */
-    protected $Session;
-
-    /**
-     * strtotime compatible string
-     * Set time how long a token is valide
-     * @var string
-     */
-    protected $csrfExpires = '+30 minutes';
-
-    /**
-     * Maximum number of tokens stored in session
-     * @var int
-     */
-    protected $csrfLimit = 150;
-
-    /**
-     * Remove CSRF tokens after use
-     * Can cause issues for Single Page applications
-     * @var boolean
-     */
-    protected $csrfUseOnce = true;
-
-    /**
-     * Token length
-     * @var int
-     */
-    protected $length = 16;
-
-    /**
-     * Token storage from and for $_SESSION
-     * @var array
-     */
-    protected $tokens = [];
-
-    /**
-     * CSRF constructor.
-     * @param \SessionComponent $Session
-     */
-    public function __construct(\SessionComponent $Session) {
-        $this->Session = $Session;
-
-        $tokens = $this->readTokens();
-        $this->tokens = $this->expireTokens($tokens);
-    }
-
-    /**
-     * @return array
-     */
-    protected function readTokens() {
-        $csrf = $this->Session->read('_csrf');
-
-        $tokens = [];
-        if (isset($csrf['csrfTokens'])) {
-            $tokens = $csrf['csrfTokens'];
-            if (!is_array($tokens)) {
-                $tokens = [];
-            }
-        }
-
-        return $tokens;
-    }
-
-    /**
-     * @param array $tokens
-     * @return array
-     */
-    protected function expireTokens($tokens) {
-        $now = time();
-        foreach ($tokens as $nonce => $expires) {
-            if ($expires < $now) {
-                unset($tokens[$nonce]);
-            }
-        }
-        $overflow = count($tokens) - $this->csrfLimit;
-        if ($overflow > 0) {
-            //Remove oldest tokens
-            asort($tokens);
-            $tokens = array_slice($tokens, $overflow + 1, null, true);
-        }
-        return $tokens;
-    }
-
-    /**
-     * @return string
-     */
-    private function _generateToken() {
-        if ($this->length < 16) {
-            throw new \SecurityException('Token length needs to be >= 16!');
-        }
-
-        return hash('sha512', openssl_random_pseudo_bytes($this->length));
-    }
-
-    /**
-     * @param \Controller $Controller
-     * @return bool
-     */
-    public function validateCsrfToken(\Controller $Controller) {
-        $requestToken = $Controller->request->data('_csrfToken');
-
-        if (!$requestToken) {
-            throw new \SecurityException('Missing CSRF token');
-        }
-
-        if (!isset($this->tokens[$requestToken])) {
-            throw new \SecurityException('CSRF token mismatch');
-        }
-
-        if ($this->tokens[$requestToken] < time()) {
-            throw new \SecurityException('CSRF token expired');
-        }
-
-        if (isset($this->tokens[$requestToken])) {
-
-            if ($this->csrfUseOnce) {
-                unset($this->tokens[$requestToken]);
-            }
-            return true;
-        }
-
-        $this->blackhole();
-    }
-
-    /**
-     * Generate a new Token stores it to $_SESSION and return the current token
-     * @return string
-     */
-    public function generateToken() {
-        $newToken = $this->_generateToken();
-        $this->tokens[$newToken] = strtotime($this->csrfExpires);
-
-        $csrf = [
-            'latestToken' => $newToken,
-            'csrfTokens'  => $this->tokens
-        ];
-
-        $this->Session->write('_csrf', $csrf);
-
-        return $newToken;
-    }
-
-
-    public function blackhole() {
-        throw new \BadRequestException(__d('cake_dev', 'The request has been black-holed'));
-    }
-
-}
diff --git a/webroot/js/scripts/controllers/Administrator/AdministratorsDebugController.js b/webroot/js/scripts/controllers/Administrator/AdministratorsDebugController.js
index 9d88836d8..bb1b38ff0 100644
--- a/webroot/js/scripts/controllers/Administrator/AdministratorsDebugController.js
+++ b/webroot/js/scripts/controllers/Administrator/AdministratorsDebugController.js
@@ -310,16 +310,16 @@ angular.module('openITCOCKPIT')
         $scope.$on('$destroy', function(){
             $scope.stop();
         });
 
         $scope.setXdebugCookie = function(){
-            $.cookie('XDEBUG_TRIGGER', 'true');
+            $.cookie('XDEBUG_TRIGGER', 'true', {secure: true});
             $scope.hasXdebugCookie = true;
         };
 
         $scope.removeXdebugCookie = function(){
-            $.removeCookie('XDEBUG_TRIGGER');
+            $.removeCookie('XDEBUG_TRIGGER', {secure: true});
             $scope.hasXdebugCookie = false;
         };
 
         //On page load
         $scope.loadTimezone();

Vulnerable code:             'httponly' => true
                'expires' => $expireAt
                '/'
<?php
// Copyright (C) <2018>  <it-novum GmbH>
//
// This file is dual licensed
//
// 1.
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, version 3 of the License.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// 2.
//  If you purchased an openITCOCKPIT Enterprise Edition you can use this file
//  under the terms of the openITCOCKPIT Enterprise Edition license agreement.
//  License agreement and license key will be shipped with the order
//  confirmation.

namespace itnovum\openITCOCKPIT\Core\Security;


class CSRF {

    /**
     * @var \SessionComponent
     */
    protected $Session;

    /**
     * strtotime compatible string
     * Set time how long a token is valide
     * @var string
     */
    protected $csrfExpires = '+30 minutes';

    /**
     * Maximum number of tokens stored in session
     * @var int
     */
    protected $csrfLimit = 150;

    /**
     * Remove CSRF tokens after use
     * Can cause issues for Single Page applications
     * @var boolean
     */
    protected $csrfUseOnce = true;

    /**
     * Token length
     * @var int
     */
    protected $length = 16;

    /**
     * Token storage from and for $_SESSION
     * @var array
     */
    protected $tokens = [];

    /**
     * CSRF constructor.
     * @param \SessionComponent $Session
     */
    public function __construct(\SessionComponent $Session) {
        $this->Session = $Session;

        $tokens = $this->readTokens();
        $this->tokens = $this->expireTokens($tokens);
    }

    /**
     * @return array
     */
    protected function readTokens() {
        $csrf = $this->Session->read('_csrf');

        $tokens = [];
        if (isset($csrf['csrfTokens'])) {
            $tokens = $csrf['csrfTokens'];
            if (!is_array($tokens)) {
                $tokens = [];
            }
        }

        return $tokens;
    }

    /**
     * @param array $tokens
     * @return array
     */
    protected function expireTokens($tokens) {
        $now = time();
        foreach ($tokens as $nonce => $expires) {
            if ($expires < $now) {
                unset($tokens[$nonce]);
            }
        }
        $overflow = count($tokens) - $this->csrfLimit;
        if ($overflow > 0) {
            //Remove oldest tokens
            asort($tokens);
            $tokens = array_slice($tokens, $overflow + 1, null, true);
        }
        return $tokens;
    }

    /**
     * @return string
     */
    private function _generateToken() {
        if ($this->length < 16) {
            throw new \SecurityException('Token length needs to be >= 16!');
        }

        return hash('sha512', openssl_random_pseudo_bytes($this->length));
    }

    /**
     * @param \Controller $Controller
     * @return bool
     */
    public function validateCsrfToken(\Controller $Controller) {
        $requestToken = $Controller->request->data('_csrfToken');

        if (!$requestToken) {
            throw new \SecurityException('Missing CSRF token');
        }

        if (!isset($this->tokens[$requestToken])) {
            throw new \SecurityException('CSRF token mismatch');
        }

        if ($this->tokens[$requestToken] < time()) {
            throw new \SecurityException('CSRF token expired');
        }

        if (isset($this->tokens[$requestToken])) {

            if ($this->csrfUseOnce) {
                unset($this->tokens[$requestToken]);
            }
            return true;
        }

        $this->blackhole();
    }

    /**
     * Generate a new Token stores it to $_SESSION and return the current token
     * @return string
     */
    public function generateToken() {
        $newToken = $this->_generateToken();
        $this->tokens[$newToken] = strtotime($this->csrfExpires);

        $csrf = [
            'latestToken' => $newToken,
            'csrfTokens'  => $this->tokens
        ];

        $this->Session->write('_csrf', $csrf);

        return $newToken;
    }


    public function blackhole() {
        throw new \BadRequestException(__d('cake_dev', 'The request has been black-holed'));
    }

}
            $.cookie('XDEBUG_TRIGGER', 'true');
            $.removeCookie('XDEBUG_TRIGGER');
Fixed code:         'secure'   => true
            'httponly' => true,
            'secure'   => true
                'expires'  => $expireAt,
                'httponly' => true,
                'secure'   => true
                '/',
                null,
                true
            $.cookie('XDEBUG_TRIGGER', 'true', {secure: true});
            $.removeCookie('XDEBUG_TRIGGER', {secure: true});