VYPR
Medium severityNVD Advisory· Published May 21, 2026

CVE-2026-8238

CVE-2026-8238

Description

Concrete CMS 9.5.0 and below is vulnerable to IDOR. The '/ccm/frontend/conversations/message_page' endpoint returns the full content of any conversation message. An unauthenticated attacker can enumerate all conversation messages, including messages from restricted pages, member-only areas, and the moderation queue. File attachments with download URLs are also exposed. The Concrete CMS security team gave this vulnerability a CVSS v.4.0 score of 6.3 with Vector CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N. Thanks Tristan Madani for reporting.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An unauthenticated IDOR in Concrete CMS 9.5.0 and below exposes full conversation content including restricted and moderated messages.

Description

Concrete CMS 9.5.0 and earlier contains an insecure direct object reference (IDOR) vulnerability in the endpoint /ccm/frontend/conversations/message_page. The endpoint returns the full content of any conversation message without proper access control. An unauthenticated attacker can enumerate all conversation messages, including those from restricted pages, member-only areas, and the moderation queue. File attachments with download URLs are also exposed.[1]

Exploitation

The endpoint does not require authentication or authorization to access individual messages. An attacker can simply iterate over message IDs to retrieve the full text and any file attachment URLs. Because the endpoint is exposed over the network and requires no privileges, the attack surface is broad. The official CVSS v4.0 score is 6.3, with vector AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N.[1]

Impact

An attacker can read private conversations, including those from member-only or restricted pages, and view messages awaiting moderation. This results in a loss of confidentiality for sensitive communications. The vulnerability does not allow modification or deletion of messages. The disclosure of file attachment URLs may also lead to unauthorized access to private files.

Mitigation

The vulnerability is fixed in Concrete CMS version 9.5.1. Users should upgrade immediately. The release notes for version 9.5.1 confirm the fix and include other security improvements such as switching Express Entry blocks to use public identifiers instead of sequential IDs.[1] No workarounds are documented.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

3
298bc027fb14

Preparing for 9.5.1

https://github.com/concretecms/concretecmsAndrew EmblerMay 19, 2026Fixed in 9.5.1via llm-release-walk
2 files changed · +3 3
  • build/tasks/build-release/download.js+1 1 modified
    @@ -3,7 +3,7 @@
     const download = require('download');
     
     module.exports = function(grunt, config, parameters, done) {
    -	var zipUrl = parameters.releaseSourceZip || 'https://github.com/concretecms/concretecms/archive/refs/tags/9.5.0.zip';
    +	var zipUrl = parameters.releaseSourceZip || 'https://github.com/concretecms/concretecms/archive/refs/tags/9.5.1.zip';
     	var workFolder = parameters.releaseWorkFolder || './release';
     	function endForError(e) {
     		process.stderr.write(e.message || e);
    
  • concrete/config/concrete.php+2 2 modified
    @@ -6,8 +6,8 @@
          *
          * @var string
          */
    -    'version' => '9.5.1a1',
    -    'version_installed' => '9.5.1a1',
    +    'version' => '9.5.1',
    +    'version_installed' => '9.5.1',
         'version_db' => '20260203004500', // the key of the latest database migration
     
         /*
    
f22b9dff5945

Sec fixes 9.5.1 (#12920)

https://github.com/concretecms/concretecmsAndrew EmblerMay 19, 2026Fixed in 9.5.1via llm-release-walk
73 files changed · +737 355
  • build/assets/themes/dashboard/js/file-manager/file-manager.js+6 0 modified
    @@ -39,6 +39,9 @@
                     new ConcreteAjaxRequest({
                         loader: false,
                         url: CCM_DISPATCHER_FILENAME + "/ccm/system/file/add_favorite_folder/" + favoriteFolderId,
    +                    data: {
    +                        ccm_token: CCM_SECURITY_TOKEN
    +                    },
                         success: function() {
                             ConcreteEvent.publish('FileManagerRefreshFavoriteFolderList')
                         }
    @@ -48,6 +51,9 @@
                     new ConcreteAjaxRequest({
                         loader: false,
                         url: CCM_DISPATCHER_FILENAME + "/ccm/system/file/remove_favorite_folder/" + favoriteFolderId,
    +                    data: {
    +                        ccm_token: CCM_SECURITY_TOKEN
    +                    },
                         success: function() {
                             ConcreteEvent.publish('FileManagerRefreshFavoriteFolderList')
                         }
    
  • build/package.json+1 1 modified
    @@ -13,7 +13,7 @@
         "production": "mix --production"
       },
       "devDependencies": {
    -    "@concretecms/bedrock": "^1.7.1",
    +    "@concretecms/bedrock": "^1.7.2",
         "cross-env": "^5.1.1",
         "download": "~8.0.0",
         "grunt": "^1.5.3",
    
  • build/package-lock.json+4 4 modified
    @@ -12,7 +12,7 @@
             "tui-image-editor": "^3.10.0"
           },
           "devDependencies": {
    -        "@concretecms/bedrock": "^1.7.1",
    +        "@concretecms/bedrock": "^1.7.2",
             "cross-env": "^5.1.1",
             "download": "~8.0.0",
             "grunt": "^1.5.3",
    @@ -1751,9 +1751,9 @@
           }
         },
         "node_modules/@concretecms/bedrock": {
    -      "version": "1.7.1",
    -      "resolved": "https://registry.npmjs.org/@concretecms/bedrock/-/bedrock-1.7.1.tgz",
    -      "integrity": "sha512-iFj4vFmDR9cEs1Is0Xx4dySNc7q5DukRFyp6oFpRGQ4K6qLVqLosvA97aZtt5Ix1OO7K2rNDQ4lNwXfYAOhvAg==",
    +      "version": "1.7.2",
    +      "resolved": "https://registry.npmjs.org/@concretecms/bedrock/-/bedrock-1.7.2.tgz",
    +      "integrity": "sha512-ZlBLoD3zLgsLf3Up6ilwCj+H3Bdxo4PCRp04g3zFg5TfTsIq0bkNKRTaQq7P/mPC3TbyXBK+7g5XobK4JGxbCQ==",
           "dev": true,
           "license": "MIT",
           "dependencies": {
    
  • concrete/blocks/calendar/controller.php+13 3 modified
    @@ -4,9 +4,11 @@
     
     use Concrete\Core\Attribute\Key\EventKey;
     use Concrete\Core\Block\BlockController;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Calendar\Calendar;
     use Concrete\Core\Calendar\CalendarServiceProvider;
     use Concrete\Core\Calendar\Event\EventOccurrenceList;
    +use Concrete\Core\Error\UserMessageException;
     use Concrete\Core\Feature\Features;
     use Concrete\Core\Feature\UsesFeatureInterface;
     use Concrete\Core\Html\Object\HeadLink;
    @@ -193,10 +195,18 @@ public function action_get_events($bID)
             $service = $this->app->make('date');
     
             if ($bID == $this->bID) {
    +            $calendar = $this->getCalendar();
    +            if (!$calendar) {
    +                throw new UserMessageException(t('Invalid calendar.'));
    +            }
    +            $checker = new Checker($calendar);
    +            if (!$checker->canViewCalendar()) {
    +                throw new UserMessageException(t('Access Denied.'));
    +            }
                 $start = $this->request->query->get('start');
                 $end = $this->request->query->get('end');
                 $list = new EventOccurrenceList();
    -            $list->filterByCalendar($this->getCalendar());
    +            $list->filterByCalendar($calendar);
                 if ($this->filterByTopicAttributeKeyID) {
                     $ak = EventKey::getByID($this->filterByTopicAttributeKeyID);
                     if (is_object($ak)) {
    @@ -424,7 +434,7 @@ public function supportsLightbox()
          */
         public function save($args)
         {
    -        if (($args['_fromCIF'] ?? null) === true) {
    +        if ($this->saveMode === SaveMode::SAVE_MODE_IMPORT) {
                 parent::save($args);
                 return;
             }
    @@ -548,7 +558,7 @@ public function export(SimpleXMLElement $blockNode)
         protected function getImportData($blockNode, $page)
         {
             $data = parent::getImportData($blockNode, $page);
    -        $data['_fromCIF'] = true;
    +        $this->saveMode = SaveMode::SAVE_MODE_IMPORT;
             $data['caID'] = 0;
             if (is_string($caName = $data['caName'] ?? null) && ($caName = trim($caName)) !== '') {
                 $calendar = Calendar::getByName($caName);
    
  • concrete/blocks/express_entry_detail/controller.php+9 1 modified
    @@ -7,12 +7,15 @@
     use Concrete\Core\Entity\Express\Entity;
     use Concrete\Core\Entity\Express\Entry;
     use Concrete\Core\Entity\Express\Form;
    +use Concrete\Core\Error\UserMessageException;
     use Concrete\Core\Express\Form\Context\FrontendViewContext;
     use Concrete\Core\Express\Form\Renderer;
    +use Concrete\Core\Express\ObjectManager;
     use Concrete\Core\Feature\Features;
     use Concrete\Core\Feature\UsesFeatureInterface;
     use Concrete\Core\Form\Context\ContextFactory;
     use Concrete\Core\Html\Service\Seo;
    +use Concrete\Core\Permission\Checker;
     use Concrete\Core\Support\Facade\Express;
     use Concrete\Core\Support\Facade\Facade;
     use Concrete\Core\Url\SeoCanonical;
    @@ -157,9 +160,14 @@ public function view()
          */
         public function action_view_express_entity($exEntryID = null)
         {
    -        $entry = $exEntryID ? $this->entityManager->find(Entry::class, $exEntryID) : null;
    +        $objectManager = $this->app->make(ObjectManager::class);
    +        $entry = $exEntryID ? $objectManager->getEntryByPublicIdentifier($exEntryID) : null;
             if ($entry) {
                 $entity = $this->entityManager->find(Entity::class, $this->exEntityID);
    +            $checker = new Checker($entity);
    +            if (!$checker->canViewExpressEntries()) {
    +                throw new UserMessageException(t('Access Denied.'));
    +            }
                 if ($entry->getEntity()->getID() == $entity->getID()) {
                     /** @var Seo $seo */
                     $seo = $this->app->make('helper/seo');
    
  • concrete/blocks/express_entry_list/controller.php+3 2 modified
    @@ -4,6 +4,7 @@
     use Concrete\Controller\Element\Search\Express\CustomizeResults;
     use Concrete\Controller\Element\Search\SearchFieldSelector;
     use Concrete\Core\Block\BlockController;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Entity\Express\Association;
     use Concrete\Core\Entity\Express\Entity;
     use Concrete\Core\Entity\Search\Query;
    @@ -477,7 +478,7 @@ protected function getItemsPerPageOptions()
         public function save($data)
         {
             $this->on_start();
    -        $fromCIF = ($data['_fromCIF'] ?? null) === true;
    +        $fromCIF = $this->saveMode === SaveMode::SAVE_MODE_IMPORT;
             if (!$fromCIF) {
                 $data['columns'] = '';
                 $data['filterFields'] = '';
    @@ -564,7 +565,7 @@ protected function getImportData($blockNode, $page)
         private function doGetImportData(SimpleXMLElement $blockNode, $page): array
         {
             $args = parent::getImportData($blockNode, $page);
    -        $args['_fromCIF'] = true;
    +        $this->saveMode = SaveMode::SAVE_MODE_IMPORT;
             $xRecord = $blockNode->data[0]->record[0];
     
             $entityID = (string) ($args['exEntityID'] ?? '');
    
  • concrete/blocks/express_entry_list/view.php+1 1 modified
    @@ -99,7 +99,7 @@ class="table ccm-block-express-entry-list-table <?php if ($tableStriped) { ?><?p
                     <tr class="<?=$rowClass?>">
                     <?php foreach ($item->getColumns() as $column) {
                         if ($controller->linkThisColumn($column)) { ?>
    -                        <td><a href="<?=URL::to($detailPage, 'view_express_entity', $item->getEntry()->getId())?>"><?=$column->getColumnValue($item);?></a></td>
    +                        <td><a href="<?=URL::to($detailPage, 'view_express_entity', $item->getEntry()->getPublicIdentifier())?>"><?=$column->getColumnValue($item);?></a></td>
                         <?php
                         } else { ?>
                             <td><?=$column->getColumnValue($item);?></td>
    
  • concrete/blocks/feature/controller.php+4 2 modified
    @@ -2,6 +2,7 @@
     namespace Concrete\Block\Feature;
     
     use Concrete\Core\Block\BlockController;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Editor\LinkAbstractor;
     use Concrete\Core\Error\UserMessageException;
     use Concrete\Core\Feature\Features;
    @@ -228,7 +229,7 @@ public function save($args)
             $args['title'] = isset($args['title']) ? $security->sanitizeString($args['title']) : '';
             $args['titleFormat'] = isset($args['titleFormat']) ? $security->sanitizeString($args['titleFormat']) : '';
             $args['paragraph'] = isset($args['paragraph']) ? LinkAbstractor::translateTo($args['paragraph']) : '';
    -        if (($args['_fromCIF'] ?? null) === true) {
    +        if ($this->saveMode === SaveMode::SAVE_MODE_IMPORT) {
                 $args['internalLinkCID'] = empty($args['internalLinkCID']) ? 0 : (int) $args['internalLinkCID'];
             } else {
                 [$linkHandle, $linkValue] = $this->app->make(DestinationPicker::class)->decode('link', $this->getLinkDestinationPickers(), $errors, t('Link'), $args);
    @@ -248,7 +249,8 @@ public function save($args)
          */
         protected function getImportData($blockNode, $page)
         {
    -        return parent::getImportData($blockNode, $page) + ['_fromCIF' => true];
    +        $this->saveMode = SaveMode::SAVE_MODE_IMPORT;
    +        return parent::getImportData($blockNode, $page);
         }
     
         /**
    
  • concrete/blocks/feature_link/controller.php+3 2 modified
    @@ -3,6 +3,7 @@
     namespace Concrete\Block\FeatureLink;
     
     use Concrete\Core\Block\BlockController;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Feature\Features;
     use Concrete\Core\Feature\UsesFeatureInterface;
     use Concrete\Core\File\File;
    @@ -272,7 +273,7 @@ public function view()
     
         public function save($args)
         {
    -        $fromCIF = ($args['__fromCIF'] ?? null) === true;
    +        $fromCIF = $this->saveMode === SaveMode::SAVE_MODE_IMPORT;
             if (!$fromCIF) {
                 list($imageLinkType, $imageLinkValue) = $this->app->make(DestinationPicker::class)->decode('imageLink', $this->getImageLinkPickers(), null, null, $args);
                 $args['buttonInternalLinkCID'] = $imageLinkType === 'page' ? $imageLinkValue : 0;
    @@ -295,8 +296,8 @@ public function save($args)
          */
         public function getImportData($blockNode, $page)
         {
    +        $this->saveMode = SaveMode::SAVE_MODE_IMPORT;
             $args = parent::getImportData($blockNode, $page);
    -        $args += ['__fromCIF' => true];
             foreach (['buttonInternalLinkCID', 'buttonFileLinkID', 'fID'] as $field) {
                 $args[$field] = empty($args[$field]) ? 0 : (int) $args[$field];
             }
    
  • concrete/blocks/hero_image/controller.php+1 0 modified
    @@ -259,6 +259,7 @@ public function save($args)
             }
     
             $args['image'] = is_numeric($args['image']) ? $args['image'] : 0;
    +        $args['height'] = isset($args['height']) ? (int) $args['height'] : 0;
             $security = $this->app->make('helper/security');
             $args['icon'] = $security->sanitizeString($args['icon']);
     
    
  • concrete/blocks/hero_image/view.php+2 0 modified
    @@ -31,6 +31,8 @@
         return;
     }
     
    +$height = (int) $height;
    +
     /**
      * Building the button
      */
    
  • concrete/blocks/image/controller.php+3 2 modified
    @@ -3,6 +3,7 @@
     namespace Concrete\Block\Image;
     
     use Concrete\Core\Block\BlockController;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Database\Connection\Connection;
     use Concrete\Core\Error\Error;
     use Concrete\Core\Feature\Features;
    @@ -547,7 +548,7 @@ public function delete()
          */
         public function save($args)
         {
    -        $fromCIF = ($args['__fromCIF'] ?? null) === true;
    +        $fromCIF = $this->saveMode === SaveMode::SAVE_MODE_IMPORT;
             /** @var Connection $db */
             $db = $this->app->make(Connection::class);
     
    @@ -611,8 +612,8 @@ public function save($args)
          */
         protected function getImportData($blockNode, $page)
         {
    +        $this->saveMode = SaveMode::SAVE_MODE_IMPORT;
             $args = parent::getImportData($blockNode, $page);
    -        $args['__fromCIF'] = true;
             foreach (['internalLinkCID', 'fileLinkID'] as $field) {
                 $args[$field] = empty($args[$field]) ? 0 : (int) $args[$field];
             }
    
  • concrete/blocks/page_list/controller.php+3 2 modified
    @@ -7,6 +7,7 @@
     use Concrete\Core\Attribute\Category\PageCategory;
     use Concrete\Core\Attribute\Key\CollectionKey;
     use Concrete\Core\Block\BlockController;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Block\View\BlockView;
     use Concrete\Core\Feature\Features;
     use Concrete\Core\Feature\UsesFeatureInterface;
    @@ -776,7 +777,7 @@ public function isValidControllerTask($method, $parameters = [])
     
         public function save($args)
         {
    -        $fromCIF = ($args['__fromCIF'] ?? null) === true;
    +        $fromCIF = $this->saveMode === SaveMode::SAVE_MODE_IMPORT;
     
             // If we've gotten to the process() function for this class, we assume that we're in
             // the clear, as far as permissions are concerned (since we check permissions at several
    @@ -1003,11 +1004,11 @@ public function export(SimpleXMLElement $blockNode)
          */
         protected function getImportData($blockNode, $page)
         {
    +        $this->saveMode = SaveMode::SAVE_MODE_IMPORT;
             if (!$this->cID && $page) {
                 $this->cID = $page->getCollectionID();
             }
             $args = parent::getImportData($blockNode, $page);
    -        $args['__fromCIF'] = true;
             $args['customTopicTreeNodeID'] = 0;
             $customTopicAttributeKeyHandle = (string) ($args['customTopicAttributeKeyHandle'] ?? '');
             $customTopicTreeNodePath = (string) ($args['customTopicTreeNodePath'] ?? '');
    
  • concrete/blocks/survey/controller.php+24 3 modified
    @@ -159,6 +159,12 @@ public function action_form_save_vote($bID = false)
             if (!$this->hasVoted()) {
                 $antispam = Core::make('helper/validation/antispam');
                 if ($antispam->check('', 'survey_block')) { // we do a blank check which will still check IP and UserAgent's
    +                $optionID = (int) $this->request->get('optionID');
    +                $q = 'SELECT optionID FROM btSurveyOptions WHERE optionID = ? AND bID = ?';
    +                $optionID = $db->getOne($q, [$optionID, $this->bID]);
    +                if (!$optionID) {
    +                    return false;
    +                }
                     $duID = 0;
                     if ($u->getUserID() > 0) {
                         $duID = $u->getUserID();
    @@ -169,7 +175,7 @@ public function action_form_save_vote($bID = false)
                     $ip = $iph->getRequestIP();
                     $ip = ($ip === false) ? ('') : ($ip->getIp($ip::FORMAT_IP_STRING));
                     $v = [
    -                    $this->request->get('optionID'),
    +                    $optionID,
                         $this->bID,
                         $duID,
                         $ip,
    @@ -219,8 +225,23 @@ public function hasVoted()
                 if ($result > 0) {
                     return true;
                 }
    -        } else if ($cookieHasVoted) {
    -            return true;
    +        } else {
    +            /** @var \Concrete\Core\Permission\IPService $iph */
    +            $iph = Core::make('helper/validation/ip');
    +            $ip = $iph->getRequestIP();
    +            $ip = ($ip === false) ? ('') : ($ip->getIp($ip::FORMAT_IP_STRING));
    +            if ($ip !== '') {
    +                $db = Database::connection();
    +                $v = [$ip, $this->bID, $this->cID];
    +                $q = 'SELECT count(resultID) AS total FROM btSurveyResults WHERE ipAddress = ? AND bID = ? AND cID = ?';
    +                $result = $db->getOne($q, $v);
    +                if ($result > 0) {
    +                    return true;
    +                }
    +            }
    +            if ($cookieHasVoted) {
    +                return true;
    +            }
             }
     
             return false;
    
  • concrete/controllers/backend/file.php+46 65 modified
    @@ -25,7 +25,10 @@
     use Concrete\Core\Permission\Checker;
     use Concrete\Core\Tree\Node\Node;
     use Concrete\Core\Tree\Node\Type\FileFolder;
    -use Concrete\Core\Url\Url;
    +use Concrete\Core\Url\Validation\InvalidRemoteUrlException;
    +use Concrete\Core\Url\Validation\RemoteUrlRequestOptionsBuilder;
    +use Concrete\Core\Url\Validation\RemoteUrlValidator;
    +use Concrete\Core\Url\Validation\ValidatedRemoteUrl;
     use Concrete\Core\Utility\Service\Number;
     use Concrete\Core\Utility\Service\Validation\Strings;
     use Concrete\Core\Validation\CSRF\Token;
    @@ -35,12 +38,7 @@
     use FileSet;
     use GuzzleHttp\Client;
     use GuzzleHttp\Psr7\Request;
    -use GuzzleHttp\RequestOptions;
    -use IPLib\Factory as IPFactory;
    -use IPLib\ParseStringFlag as IPParseStringFlag;
    -use IPLib\Range\Type as IPRangeType;
     use Permissions as ConcretePermissions;
    -use RuntimeException;
     use Symfony\Component\HttpFoundation\File\UploadedFile;
     use ZipArchive;
     
    @@ -69,6 +67,10 @@ class File extends Controller
     
         public function star()
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate()) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             $fs = FileSet::createAndGetSet('Starred Files', FileSet::TYPE_STARRED);
             $files = $this->getRequestFiles();
             $r = new FileEditResponse();
    @@ -87,6 +89,10 @@ public function star()
     
         public function rescan()
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate()) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             $files = $this->getRequestFiles('edit_file_contents');
             $r = new FileEditResponse();
             $r->setFiles($files);
    @@ -106,6 +112,10 @@ public function rescan()
     
         public function rescanMultiple()
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate()) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             $files = $this->getRequestFiles('edit_file_contents');
             $batch = BatchBuilder::create(t('Rescan Files'), function() use ($files) {
                 foreach ($files as $file) {
    @@ -117,6 +127,10 @@ public function rescanMultiple()
     
         public function approveVersion()
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate('approve_file_version')) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             $files = $this->getRequestFiles('edit_file_contents');
             $fvID = $this->request->request->get('fvID', $this->request->query->get('fvID'));
             $fvID = $this->app->make('helper/security')->sanitizeInt($fvID);
    @@ -133,9 +147,7 @@ public function approveVersion()
         public function deleteVersion()
         {
             $token = $this->app->make('token');
    -        if (!$token->validate('delete-version')) {
    -            $files = $this->getRequestFiles('edit_file_contents');
    -        }
    +        $files = $this->getRequestFiles('edit_file_contents');
             $fvID = $this->request->request->get('fvID', $this->request->query->get('fvID'));
             $fvID = $this->app->make('helper/security')->sanitizeInt($fvID);
             $fv = $files[0]->getVersion($fvID);
    @@ -254,6 +266,10 @@ public function getFavoriteFolders()
     
         public function addFavoriteFolder($folderId)
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate()) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             $editResponse = new EditResponse();
             $errors = new ErrorList();
             $user = new \Concrete\Core\User\User();
    @@ -301,6 +317,10 @@ public function addFavoriteFolder($folderId)
         /** @noinspection DuplicatedCode */
         public function removeFavoriteFolder($folderId)
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate()) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             $editResponse = new EditResponse();
             $errors = new ErrorList();
             $user = new \Concrete\Core\User\User();
    @@ -421,15 +441,15 @@ public function importRemote()
                         break;
                 }
     
    -            $validIps = (array) $this->checkRemoteURlsToImport($urls);
    +            $validatedUrls = (array) $this->checkRemoteURlsToImport($urls);
     
                 $originalPage = $this->getImportOriginalPage();
                 $fi = $this->app->make(Importer::class);
                 $volatileDirectory = $this->app->make(VolatileDirectory::class);
                 foreach ($urls as $url) {
                     try {
                         $host = (string) \League\Url\Url::createFromUrl($url)->getHost();
    -                    $downloadedFile = $this->downloadRemoteURL($url, $volatileDirectory->getPath(), $validIps[$host] ?? null);
    +                    $downloadedFile = $this->downloadRemoteURL($url, $volatileDirectory->getPath(), $validatedUrls[$host] ?? null);
                         $fileVersion = $fi->import($downloadedFile, false, $replacingFile ?: $this->getDestinationFolder());
                         if (!$fileVersion instanceof FileVersionEntity) {
                             $errors->add($url . ': ' . $fi->getErrorMessage($fileVersion));
    @@ -777,65 +797,37 @@ protected function checkExistingIncomingFiles(array $incomingFiles, Incoming $in
         /**
          * Check that a list of strings are valid "incoming" file names.
          *
    -     * @param string $urls
    -     * @return array<string, string> An array of domains and their validated IPs
    +     * @param string[] $urls
    +     * @return array<string, \Concrete\Core\Url\Validation\ValidatedRemoteUrl> An array of domains and their validated URLs
          *
          * @throws \Concrete\Core\Error\UserMessageException in case one or more of the specified URLs are not valid
          *
          * @since 8.5.0a3
          */
         protected function checkRemoteURlsToImport(array $urls)
         {
    -        $validIps = [];
    +        $validator = new RemoteUrlValidator();
    +        $validatedUrls = [];
             foreach ($urls as $u) {
                 try {
    -                $url = Url::createFromUrl($u);
    -            } catch (RuntimeException $x) {
    -                throw new UserMessageException(h(t('The URL "%s" is not valid: %s', $u, $x->getMessage())));
    -            }
    -            $scheme = (string)$url->getScheme();
    -            if ($scheme === '') {
    -                throw new UserMessageException(h(t('The URL "%s" is not valid.', $u)));
    -            }
    -            $host = trim((string)$url->getHost());
    -            if (in_array(strtolower($host), ['', '0', 'localhost'], true)) {
    +                $validatedUrl = $validator->validate((string) $u);
    +            } catch (InvalidRemoteUrlException $x) {
    +                if ($x->getPrevious() !== null) {
    +                    throw new UserMessageException(h(t('The URL "%s" is not valid: %s', $u, $x->getMessage())));
    +                }
                     throw new UserMessageException(h(t('The URL "%s" is not valid.', $u)));
                 }
    +            $host = $validatedUrl->getHost();
     
                 // If we've already validated this hostname just skip it.
    -            if (array_key_exists($host, $validIps)) {
    +            if (array_key_exists($host, $validatedUrls)) {
                     continue;
                 }
     
    -            $ipFormatBlocks = [
    -                '/^\d+$/', // No fully integer / octal hostnames http://2130706433 http://017700000001
    -                '/^0x[0-9a-f]+$/i', // No Hexadecimal hostnames http://0x07f000001
    -            ];
    -
    -            foreach ($ipFormatBlocks as $block) {
    -                if (preg_match($block, $host) !== 0) {
    -                    throw new UserMessageException(h(t('The URL "%s" is not valid.', $u)));
    -                }
    -            }
    -
    -            $ipFlags = IPParseStringFlag::IPV4_MAYBE_NON_DECIMAL | IPParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED | IPParseStringFlag::MAY_INCLUDE_PORT | IPParseStringFlag::MAY_INCLUDE_ZONEID;
    -            $ip = IPFactory::parseAddressString($host, $ipFlags);
    -            if ($ip === null) {
    -                $dnsList = @dns_get_record($host, DNS_A | DNS_AAAA);
    -                while ($ip === null && $dnsList !== false && count($dnsList) > 0) {
    -                    $dns = array_shift($dnsList);
    -                    $ip = IPFactory::parseAddressString($dns['ip']);
    -                }
    -            }
    -
    -            if ($ip !== null && $ip->getRangeType() !== IPRangeType::T_PUBLIC) {
    -                throw new UserMessageException(h(t('The URL "%s" is not valid.', $u)));
    -            }
    -
    -            $validIps[$host] = $ip->toString();
    +            $validatedUrls[$host] = $validatedUrl;
             }
     
    -        return $validIps;
    +        return $validatedUrls;
         }
     
         /**
    @@ -848,24 +840,13 @@ protected function checkRemoteURlsToImport(array $urls)
          * @throws \Concrete\Core\Error\UserMessageException in case of errors
          *
          */
    -    protected function downloadRemoteURL($url, $temporaryDirectory, ?string $ip = null)
    +    protected function downloadRemoteURL($url, $temporaryDirectory, ?ValidatedRemoteUrl $validatedUrl = null)
         {
             /** @var Client $client */
             $client = $this->app->make(Client::class);
             $request = new Request('GET', $url);
     
    -        $config = [
    -            RequestOptions::ALLOW_REDIRECTS => false,
    -        ];
    -
    -        if ($ip) {
    -            $host = parse_url($url, PHP_URL_HOST);
    -            $scheme = parse_url($url, PHP_URL_SCHEME);
    -            $port = parse_url($url, PHP_URL_PORT) ?: ($scheme === 'http' ? 80 : 443);
    -
    -            // Specify IP if one is provided.
    -            $config['curl'] = [CURLOPT_RESOLVE => ["{$host}:{$port}:{$ip}"]];
    -        }
    +        $config = $validatedUrl ? (new RemoteUrlRequestOptionsBuilder())->build($validatedUrl) : [];
     
             $response = $client->send($request, $config);
     
    
  • concrete/controllers/backend/page/sitemap_delete_forever.php+4 0 modified
    @@ -19,6 +19,10 @@ public function canAccess()
     
         public function fillQueue()
         {
    +        $token = $this->app->make('token');
    +        if (!$token->validate()) {
    +            throw new UserMessageException($token->getErrorMessage(), 401);
    +        }
             if ($this->canAccess()) {
                 $c = Page::getByID($_REQUEST['cID']);
                 if (is_object($c) && !$c->isError()) {
    
  • concrete/controllers/backend/page/type/composer/form/edit_control/save.php+11 1 modified
    @@ -63,8 +63,18 @@ protected function getPostedLabel(): string
         protected function getPostedTemplate(): string
         {
             $template = $this->request->request->get('ptComposerFormLayoutSetControlCustomTemplate');
    +        if (!is_string($template)) {
    +            return '';
    +        }
    +
    +        $setControl = $this->getSetControl();
    +        $control = $this->getControl($setControl);
    +        $templates = $this->getTemplates($control);
    +        if (!array_key_exists($template, $templates)) {
    +            return '';
    +        }
     
    -        return is_string($template) ? $this->app->make(SanitizeService::class)->sanitizeString($template) : '';
    +        return $this->app->make(SanitizeService::class)->sanitizeString($template);
         }
     
         protected function getPostedDescription(): string
    
  • concrete/controllers/backend/summary_template.php+7 0 modified
    @@ -2,6 +2,7 @@
     namespace Concrete\Controller\Backend;
     
     use Concrete\Core\Controller\Controller;
    +use Concrete\Core\Error\UserMessageException;
     use Concrete\Core\Page\Theme\Theme;
     use Concrete\Core\Summary\Category\Driver\DriverInterface;
     use Concrete\Core\Summary\Category\Driver\Manager;
    @@ -30,6 +31,12 @@ public function render($categoryHandle, $memberIdentifier, $templateID)
              */
             $template = $category->getMemberSummaryTemplate($templateID);
             $object = $category->getCategoryMemberFromIdentifier($memberIdentifier);
    +        if (!$object) {
    +            throw new UserMessageException(t('Unable to locate object.'));
    +        }
    +        if (!$category->canViewRenderedSummaryTemplates($object)) {
    +            throw new UserMessageException(t('Access Denied.'));
    +        }
             $renderer = $this->app->make(Renderer::class);
     
             $this->set('template', $template);
    
  • concrete/controllers/dialog/event/duplicate.php+1 1 modified
    @@ -21,7 +21,7 @@ class Duplicate extends BackendInterfaceController
     
         public function submit()
         {
    -        if ($this->canAccess()) {
    +        if ($this->validateAction()) {
     
                 $e = $this->app->make('error');
                 $calendar = $this->app->make(CalendarService::class)->getByID($_REQUEST['caID']);
    
  • concrete/controllers/dialog/express/association/reorder.php+37 31 modified
    @@ -14,7 +14,7 @@ protected function canAccess()
             $entry = $this->getEntry();
             if ($entry) {
                 $ep = new \Permissions($entry);
    -            return $ep->canViewExpressEntry();
    +            return $ep->canEditExpressEntry();
             }
             return false;
         }
    @@ -63,39 +63,45 @@ public function view()
     
         public function submit()
         {
    -        $em = \Database::connection()->getEntityManager();
    -        $selectedEntry = $this->getEntry();
    -        $control = $this->getControl();
    -        if ($association = $control->getAssociation()) {
    -            $i = 0;
    -            $handler = $association->getSaveHandler();
    -            /**
    -             * @var $handler ManySaveHandlerInterface
    -             */
    -            $supportsCustomDisplayOrder = false;
    -            if ($association->getTargetEntity()->supportsCustomDisplayOrder()) {
    -                $supportsCustomDisplayOrder = true;
    -            }
    +        if ($this->validateAction()) {
    +            $em = \Database::connection()->getEntityManager();
    +            $selectedEntry = $this->getEntry();
    +            $control = $this->getControl();
    +            if ($association = $control->getAssociation()) {
    +                $i = 0;
    +                $handler = $association->getSaveHandler();
    +                /**
    +                 * @var $handler ManySaveHandlerInterface
    +                 */
    +                $supportsCustomDisplayOrder = false;
    +                if ($association->getTargetEntity()->supportsCustomDisplayOrder()) {
    +                    $supportsCustomDisplayOrder = true;
    +                }
    +
    +                $associatedEntries = $handler->getAssociatedEntriesFromRequest($control, $this->request);
    +                $entryAssociation = $selectedEntry->getAssociation($association);
    +                foreach($associatedEntries as $entry) {
    +                    $associationEntry = $entryAssociation ? $entryAssociation->getAssociationEntry($entry) : null;
    +                    if (!$associationEntry) {
    +                        continue;
    +                    }
     
    -            $associatedEntries = $handler->getAssociatedEntriesFromRequest($control, $this->request);
    -            foreach($associatedEntries as $entry) {
    -                if ($supportsCustomDisplayOrder) {
    -                    $entry->setEntryDisplayOrder($i);
    -                    $em->persist($entry);
    -                } else {
    -                    $entryAssociation = $selectedEntry->getAssociation($association);
    -                    $associationEntry = $entryAssociation->getAssociationEntry($entry);
    -                    $associationEntry->setDisplayOrder($i);
    -                    $em->persist($associationEntry);
    +                    if ($supportsCustomDisplayOrder) {
    +                        $entry->setEntryDisplayOrder($i);
    +                        $em->persist($entry);
    +                    } else {
    +                        $associationEntry->setDisplayOrder($i);
    +                        $em->persist($associationEntry);
    +                    }
    +                    $i++;
                     }
    -                $i++;
    -            }
     
    -            $em->flush();
    -            $this->flash('success', t('Display order saved successfully.'));
    -            $response = new EditResponse();
    -            $response->setRedirectURL(\URL::to('/dashboard/express/entries/', 'view_entry', $selectedEntry->getId()));
    -            $response->outputJSON();
    +                $em->flush();
    +                $this->flash('success', t('Display order saved successfully.'));
    +                $response = new EditResponse();
    +                $response->setRedirectURL(\URL::to('/dashboard/express/entries/', 'view_entry', $selectedEntry->getId()));
    +                $response->outputJSON();
    +            }
             }
         }
     
    
  • concrete/controllers/dialog/file/usage.php+0 49 removed
    @@ -1,49 +0,0 @@
    -<?php
    -
    -namespace Concrete\Controller\Dialog\File;
    -
    -use Concrete\Core\Controller\Controller;
    -use Concrete\Core\Entity\Statistics\UsageTracker\FileUsageRecord;
    -use Doctrine\ORM\EntityManagerInterface;
    -
    -class Usage extends Controller
    -{
    -
    -    protected $viewPath = '/dialogs/file/usage';
    -
    -    /**
    -     * @var \Doctrine\ORM\EntityManagerInterface
    -     */
    -    protected $manager;
    -
    -    /**
    -     * Usage constructor.
    -     * @param \Doctrine\ORM\EntityManagerInterface $manager
    -     */
    -    public function __construct(EntityManagerInterface $manager)
    -    {
    -        $this->manager = $manager;
    -        parent::__construct();
    -    }
    -
    -    public function view($fID)
    -    {
    -        $records = $this->manager->getRepository(FileUsageRecord::class)->findByFile($fID);
    -        $reduced = [];
    -
    -        /** @var FileUsageRecord $record */
    -        foreach ($records as $record) {
    -            $cID = $record->getCollectionId();
    -            if (isset($reduced[$cID])) {
    -                if ($record->getCollectionVersionId() > $reduced[$cID]->getCollectionVersionId()) {
    -                    $reduced[$cID] = $record;
    -                }
    -            } else {
    -                $reduced[$cID] = $record;
    -            }
    -        }
    -
    -        $this->set('records', $reduced);
    -    }
    -
    -}
    
  • concrete/controllers/dialog/frontend/event.php+14 0 modified
    @@ -1,9 +1,12 @@
     <?php
     namespace Concrete\Controller\Dialog\Frontend;
     
    +use Concrete\Block\Calendar\Controller;
     use Concrete\Controller\Backend\UserInterface as BackendInterfaceController;
     use Concrete\Core\Block\Block;
     use Concrete\Core\Calendar\Event\EventOccurrence;
    +use Concrete\Core\Error\UserMessageException;
    +use Concrete\Core\Permission\Checker;
     
     class Event extends BackendInterfaceController
     {
    @@ -16,6 +19,17 @@ public function view($bID, $occurrence_id)
     
             if (is_object($b) && $b->getBlockTypeHandle() == 'calendar') {
                 $controller = $b->getController();
    +            /**
    +             * @var $controller Controller
    +             */
    +            $calendar = $controller->getCalendar();
    +            if (!$calendar) {
    +                throw new UserMessageException(t('Invalid calendar.'));
    +            }
    +            $checker = new Checker($calendar);
    +            if (!$checker->canViewCalendar()) {
    +                throw new UserMessageException(t('Access Denied.'));
    +            }
                 $occurrence = EventOccurrence::getByID($occurrence_id);
                 $this->set('occurrence', $occurrence);
                 $this->set('blockController', $controller);
    
  • concrete/controllers/dialog/logs/bulk/delete.php+1 1 modified
    @@ -33,7 +33,7 @@ public function view()
     
         public function submit()
         {
    -        if ($this->canAccess()) {
    +        if ($this->validateAction()) {
                 /** @var Request $request */
                 $request = $this->app->make(Request::class);
                 /** @var Connection $db */
    
  • concrete/controllers/dialog/logs/delete.php+3 0 modified
    @@ -39,6 +39,9 @@ public function view()
     
         public function submit()
         {
    +        if (!$this->validateAction()) {
    +            throw new \RuntimeException(implode("\n", $this->error->getList()));
    +        }
             /** @var Request $request */
             $request = $this->app->make(Request::class);
             /** @var Connection $db */
    
  • concrete/controllers/dialog/page/bulk/cache.php+1 1 modified
    @@ -103,7 +103,7 @@ public function view()
     
         public function submit()
         {
    -        if ($this->canAccess()) {
    +        if ($this->validateAction()) {
                 foreach ($this->items as $page) {
                     $data = array();
                     if (($cCacheFullPageContent = $this->request->request->getInt('cCacheFullPageContent')) > -2) {
    
  • concrete/controllers/dialog/page/bulk/delete.php+1 1 modified
    @@ -38,7 +38,7 @@ public function view()
     
         public function submit()
         {
    -        if ($this->canAccess()) {
    +        if ($this->validateAction()) {
                 $u = new \User();
                 $uID = $u->getUserID();
                 $pages = $this->items;
    
  • concrete/controllers/dialog/page/bulk/design.php+1 1 modified
    @@ -110,7 +110,7 @@ public function view()
     
         public function submit()
         {
    -        if ($this->canAccess()) {
    +        if ($this->validateAction()) {
                 $containsSinglePages = false;
                 foreach ($this->items as $page) {
                     if ($page->isGeneratedCollection()) {
    
  • concrete/controllers/frontend/conversations/add_file.php+1 1 modified
    @@ -32,7 +32,7 @@ public function view(): Response
                 $fileVersion = $this->importFile($conversation, $file);
     
                 return $responseFactory->json([
    -                'id' => (int) $fileVersion->getFileID(),
    +                'id' => $fileVersion->getFileUUID(),
                     'timestamp' => $post->get('timestamp'),
                     'tag' => $post->get('tag'),
                 ]);
    
  • concrete/controllers/frontend/conversations/add_message.php+6 4 modified
    @@ -201,7 +201,7 @@ protected function getMessageBody(ArrayAccess $errors): string
         }
     
         /**
    -     * @return int[]
    +     * @return string[]
          */
         protected function getAttachmentIDs(): array
         {
    @@ -213,8 +213,8 @@ protected function getAttachmentIDs(): array
             return array_values( // Reset array indexes
                 array_unique( // Remove duplicates
                     array_filter( // Remove zeroes
    -                    array_map( // Ensure integer types
    -                        'intval',
    +                    array_map( // Ensure string types
    +                        'strval',
                             $attachmentIDs
                         )
                     )
    @@ -245,9 +245,11 @@ protected function getAttachments(ArrayAccess $errors): array
                 } else {
                     $em = $this->app->make(EntityManagerInterface::class);
                     foreach ($attachmentIDs as $attachmentID) {
    -                    $file = $em->find(File::class, $attachmentID);
    +                    $file = $em->getRepository(File::class)->findOneBy(['fUUID' => $attachmentID]);
                         if ($file === null) {
                             $errors[] = t('Invalid file specified.');
    +                    } elseif (!(new Checker($file))->canViewFile()) {
    +                        $errors[] = t('Invalid file specified.');
                         } else {
                             $attachments[] = $file;
                         }
    
  • concrete/controllers/frontend/conversations/delete_file.php+1 1 modified
    @@ -29,7 +29,7 @@ public function view(): Response
     
             /** @var Token $token */
             $token = $this->app->make(Token::class);
    -        if ($token->validate("delete_conversation_message",$this->request->request->get('token'))) {
    +        if (!$token->validate("delete_conversation_message",$this->request->request->get('token'))) {
                 throw new UserMessageException($token->getErrorMessage());
             }
     
    
  • concrete/controllers/frontend/conversations/get_rating.php+3 0 modified
    @@ -43,6 +43,9 @@ protected function getMessage(): Message
             if ($message === null) {
                 throw new UserMessageException(t('Invalid message object.'));
             }
    +        if ($this->getBlockConversation()->getConversationID() != $message->getConversationObject()->getConversationID()) {
    +            throw new UserMessageException(t('Invalid Conversation.'));
    +        }
     
             return $message;
         }
    
  • concrete/controllers/frontend/conversations/message_detail.php+3 0 modified
    @@ -47,6 +47,9 @@ protected function getMessage(): Message
             if ($message === null) {
                 throw new UserMessageException(t('Invalid message object.'));
             }
    +        if ($this->getBlockConversation()->getConversationID() != $message->getConversationObject()->getConversationID()) {
    +            throw new UserMessageException(t('Invalid Conversation.'));
    +        }
     
             return $message;
         }
    
  • concrete/controllers/frontend/conversations/message_page.php+2 3 modified
    @@ -54,9 +54,8 @@ protected function getConversationID(): ?int
          */
         protected function getConversation(): Conversation
         {
    -        $conversationID = $this->getConversationID();
    -        $conversation = $conversationID === null ? null : Conversation::getByID($conversationID);
    -        if ($conversation === null) {
    +        $conversation = $this->getBlockConversation();
    +        if ((int) $conversation->getConversationID() !== $this->getConversationID()) {
                 throw new UserMessageException(t('Invalid Conversation.'));
             }
     
    
  • concrete/controllers/frontend/conversations/update_message.php+6 4 modified
    @@ -105,7 +105,7 @@ protected function getMessageBody(ArrayAccess $errors): string
         }
     
         /**
    -     * @return int[]
    +     * @return string[]
          */
         protected function getAttachmentIDs(): array
         {
    @@ -117,8 +117,8 @@ protected function getAttachmentIDs(): array
             return array_values( // Reset array indexes
                 array_unique( // Remove duplicates
                     array_filter( // Remove zeroes
    -                    array_map( // Ensure integer types
    -                        'intval',
    +                    array_map( // Ensure string types
    +                        'strval',
                             $attachmentIDs
                         )
                     )
    @@ -150,9 +150,11 @@ protected function getAttachments(Message $message, ArrayAccess $errors): array
                 } else {
                     $em = $this->app->make(EntityManagerInterface::class);
                     foreach ($attachmentIDs as $attachmentID) {
    -                    $file = $em->find(File::class, $attachmentID);
    +                    $file = $em->getRepository(File::class)->findOneBy(['fUUID' => $attachmentID]);
                         if ($file === null) {
                             $errors[] = t('Invalid file specified.');
    +                    } elseif (!(new Checker($file))->canViewFile()) {
    +                        $errors[] = t('Invalid file specified.');
                         } else {
                             $attachments[] = $file;
                         }
    
  • concrete/controllers/single_page/account/edit_profile.php+18 10 modified
    @@ -110,28 +110,31 @@ public function save()
     
             $valt = $app->make('token');
     
    -        $data = $this->post();
    +        $data = [];
     
             if (!$valt->validate('profile_edit')) {
                 $this->error->add($valt->getErrorMessage());
             }
     
             // validate the user's email
    -        $email = $this->post('uEmail');
    +        $email = (string) $this->post('uEmail');
             $app->make('validator/user/email')->isValidFor($email, $ui, $this->error);
     
             // Username validation
    -        $username = $this->post('uName');
    -        if (array_key_exists('uName', $data)) {
    -            $data['uName'] = (string) $data['uName'];
    +        $username = (string) $this->post('uName');
    +        $displayUserName = !$this->displayUserName
    +            ? false
    +            : $app->make(Repository::class)->get('concrete.user.edit_profile.display_username_field');
    +        if ($displayUserName && $this->request->request->has('uName')) {
    +            $data['uName'] = $username;
                 $app->make('validator/user/name')->isValidFor($username, $ui, $this->error);
             }
     
             // password
    -        if (strlen($data['uPasswordNew'])) {
    -            $passwordCurrent = (string) $data['uPasswordCurrent'];
    -            $passwordNew = $data['uPasswordNew'];
    -            $passwordNewConfirm = $data['uPasswordNewConfirm'];
    +        $passwordNew = (string) $this->post('uPasswordNew');
    +        if ($passwordNew !== '') {
    +            $passwordCurrent = (string) $this->post('uPasswordCurrent');
    +            $passwordNewConfirm = (string) $this->post('uPasswordNewConfirm');
     
                 $app->make('validator/password')->isValidFor($passwordNew, $ui, $this->error);
     
    @@ -144,6 +147,7 @@ public function save()
                         $this->error->add(t('The two passwords provided do not match.'));
                     }
                 }
    +            $data['_allowPasswordUpdate'] = true;
                 $data['uPasswordConfirm'] = $passwordNew;
                 $data['uPassword'] = $passwordNew;
             }
    @@ -164,7 +168,11 @@ public function save()
                 $data['uEmail'] = $email;
                 $config = $this->app->make('config');
                 if ($config->get('concrete.misc.user_timezones')) {
    -                $data['uTimezone'] = $this->post('uTimezone');
    +                $data['uTimezone'] = (string) $this->post('uTimezone');
    +            }
    +            $languages = Localization::getAvailableInterfaceLanguages();
    +            if (count($languages) > 0) {
    +                $data['uDefaultLanguage'] = (string) $this->post('uDefaultLanguage');
                 }
     
                 $ui->saveUserAttributesForm($aks);
    
  • concrete/controllers/single_page/dashboard/extend/install.php+27 7 modified
    @@ -166,13 +166,31 @@ public function package_uninstalled()
         public function install_package($package)
         {
             $this->view();
    +        if (!$this->request->isMethod('POST')) {
    +            $this->error->add(t('Invalid request method.'));
    +            return;
    +        }
             $tp = new TaskPermission();
             if ($tp->canInstallPackages()) {
                 $packageService = $this->app->make(PackageService::class);
                 $p = $packageService->getClass($package);
                 if ($p instanceof BrokenPackage) {
                     $this->error->add($p->getInstallErrorMessage());
                 } elseif (is_object($p)) {
    +                $showInstallOptionsScreen = $p->showInstallOptionsScreen();
    +                $isInitialInstallRequest = $this->token->validate('install_package');
    +                $isInstallOptionsSubmission = $showInstallOptionsScreen && $this->token->validate('install_options_selected');
    +                if ($showInstallOptionsScreen && $isInitialInstallRequest && !$isInstallOptionsSubmission) {
    +                    $this->set('showInstallOptionsScreen', true);
    +                    $this->set('pkg', $p);
    +
    +                    return;
    +                }
    +                if ((!$showInstallOptionsScreen && !$isInitialInstallRequest) || ($showInstallOptionsScreen && !$isInstallOptionsSubmission)) {
    +                    $this->error->add(t('Invalid token.'));
    +
    +                    return;
    +                }
                     $config = $this->app->make('config');
                     if ($config->get('concrete.i18n.auto_install_package_languages')) {
                         $connection = $this->getConnection();
    @@ -195,10 +213,7 @@ function (RemotePackage $rp) use ($p) {
                             }
                         }
                     }
    -                if (
    -                    (!$p->showInstallOptionsScreen()) ||
    -                    $this->token->validate('install_options_selected')
    -                ) {
    +                if (!$showInstallOptionsScreen || $isInstallOptionsSubmission) {
                         $tests = $p->testForInstall();
                         if (is_object($tests)) {
                             $this->error->add($tests);
    @@ -214,9 +229,6 @@ function (RemotePackage $rp) use ($p) {
                                 $this->redirect('/dashboard/extend/install', 'package_installed', $r->getPackageID());
                             }
                         }
    -                } else {
    -                    $this->set('showInstallOptionsScreen', true);
    -                    $this->set('pkg', $p);
                     }
                 } else {
                     $this->error->add(t('Package controller file not found.'));
    @@ -237,6 +249,14 @@ public function package_installed($pkgID = 0)
         public function download($remoteId = null)
         {
             $this->view();
    +        if (!$this->request->isMethod('POST')) {
    +            $this->error->add(t('Invalid request method.'));
    +            return;
    +        }
    +        if (!$this->token->validate('download_package')) {
    +            $this->error->add($this->token->getErrorMessage());
    +            return;
    +        }
             $tp = new TaskPermission();
             if (!$tp->canInstallPackages()) {
                 $this->error->add(t('You do not have permission to download add-ons.'));
    
  • concrete/controllers/single_page/dashboard/extend/update.php+42 21 modified
    @@ -13,6 +13,30 @@
     
     class Update extends DashboardPageController
     {
    +    protected function updatePackage(string $pkgHandle): void
    +    {
    +        $packageService = $this->app->make(PackageService::class);
    +        $packageController = $packageService->getClass($pkgHandle);
    +        $testResult = $packageController->testForUpgrade();
    +        if ($testResult !== true) {
    +            $this->error->add($testResult);
    +
    +            return;
    +        }
    +        $previousVersion = $packageController->getPackageEntity()->getPackageVersion();
    +        Localization::getInstance()->withContext(Localization::CONTEXT_SYSTEM, static function () use ($packageController) {
    +            $packageController->upgradeCoreData();
    +            $packageController->upgrade();
    +        });
    +        $this->set('message',
    +            t('Package "%1$s" has been updated successfully from version %2$s to version %3$s.',
    +                t($packageController->getPackageName()) ?: $packageController->getPackageHandle(),
    +                $previousVersion,
    +                $packageController->getPackageVersion()
    +            )
    +        );
    +    }
    +
         public function view()
         {
             $packageRepository = $this->app->make(PackageRepositoryInterface::class);
    @@ -42,31 +66,20 @@ public function do_update($pkgHandle = false)
             if (!$pkgHandle) {
                 return $this->view();
             }
    +        if (!$this->request->isMethod('POST')) {
    +            $this->error->add(t('Invalid request method.'));
    +
    +            return $this->view();
    +        }
             try {
    +            if (!$this->token->validate('update_addon')) {
    +                throw new UserMessageException($this->token->getErrorMessage());
    +            }
                 $tp = new Checker();
                 if (!$tp->canInstallPackages()) {
                     throw new UserMessageException(t('Access Denied.'));
                 }
    -            $packageService = $this->app->make(PackageService::class);
    -            $packageController = $packageService->getClass($pkgHandle);
    -            $testResult = $packageController->testForUpgrade();
    -            if ($testResult !== true) {
    -                $this->error->add($testResult);
    -
    -                return $this->view();
    -            }
    -            $previousVersion = $packageController->getPackageEntity()->getPackageVersion();
    -            Localization::getInstance()->withContext(Localization::CONTEXT_SYSTEM, static function () use ($packageController) {
    -                $packageController->upgradeCoreData();
    -                $packageController->upgrade();
    -            });
    -            $this->set('message',
    -                t('Package "%1$s" has been updated successfully from version %2$s to version %3$s.',
    -                    t($packageController->getPackageName()) ?: $packageController->getPackageHandle(),
    -                    $previousVersion,
    -                    $packageController->getPackageVersion()
    -                )
    -            );
    +            $this->updatePackage($pkgHandle);
             } catch (UserMessageException $x) {
                 $this->error->add($x);
             }
    @@ -77,8 +90,16 @@ public function prepare_remote_upgrade($remoteMPID = 0)
         {
             $packageRepository = $this->app->make(PackageRepositoryInterface::class);
             $packageService = $this->app->make(PackageService::class);
    +        if (!$this->request->isMethod('POST')) {
    +            $this->error->add(t('Invalid request method.'));
    +
    +            return $this->view();
    +        }
     
             try {
    +            if (!$this->token->validate('prepare_remote_upgrade')) {
    +                throw new UserMessageException($this->token->getErrorMessage());
    +            }
                 $tp = new Checker();
                 if (!$tp->canInstallPackages()) {
                     throw new UserMessageException(t('Access Denied.'));
    @@ -99,7 +120,7 @@ public function prepare_remote_upgrade($remoteMPID = 0)
                 }
     
                 $packageRepository->download($connection, $mri, true);
    -            return $this->buildRedirect(['/dashboard/extend/update', 'do_update', $mri->handle]);
    +            $this->updatePackage($mri->handle);
             } catch (UserMessageException $x) {
                 $this->error->add($x);
             }
    
  • concrete/controllers/single_page/dashboard/system/update/update.php+5 1 modified
    @@ -168,7 +168,11 @@ public function do_update()
             if (!$this->userHasUpgradePermission()) {
                 return $this->buildRedirect($this->action());
             }
    -        if ($this->app->make(Composer::class)->isCoreInstalledViaComposer() === true) {
    +        if (!$this->request->isMethod('POST')) {
    +            $this->error->add(t('Invalid request method.'));
    +        } elseif (!$this->token->validate('do_update')) {
    +            $this->error->add($this->token->getErrorMessage());
    +        } elseif ($this->app->make(Composer::class)->isCoreInstalledViaComposer() === true) {
                 $this->error->add(t('ConcreteCMS has been installed via Composer: you should use it to upgrade the currently installed version.'));
             } elseif ($this->app->make('config')->get('concrete.updates.skip_core')) {
                 $this->error->add(t('Updates are currently disabled via your site configuration.'));
    
  • concrete/controllers/single_page/dashboard/users/groups/bulk_user_assignment.php+10 2 modified
    @@ -7,6 +7,7 @@
     use Concrete\Core\Logging\Channels;
     use Concrete\Core\Logging\LoggerFactory;
     use Concrete\Core\Page\Controller\DashboardPageController;
    +use Concrete\Core\Permission\Checker;
     use Concrete\Core\User\Group\Group;
     use Concrete\Core\User\Group\GroupRepository;
     use Concrete\Core\User\UserInfo;
    @@ -43,6 +44,15 @@ public function view()
                     $removeUnlistedUsers = $this->request->request->has('removeUnlistedUsers');
     
                     if ($targetGroup instanceof Group) {
    +                    $gp = new Checker($targetGroup);
    +                    if (!$gp->canAssignGroup()) {
    +                        $this->error->add(t('Access Denied.'));
    +                    }
    +                } else {
    +                    $this->error->add(t('You need to select valid target group.'));
    +                }
    +
    +                if (!$this->error->has()) {
                         /** @var UploadedFile $csvFile */
                         $csvFile = $this->request->files->get('csvFile');
     
    @@ -74,8 +84,6 @@ public function view()
                         } else {
                             $this->error->add(t('You need to upload a CSV file.'));
                         }
    -                } else {
    -                    $this->error->add(t('You need to select valid target group.'));
                     }
     
                     if (!$this->error->has()) {
    
  • concrete/controllers/single_page/dashboard/users/search.php+2 0 modified
    @@ -399,6 +399,7 @@ public function save_account($uID = null)
                             $error->add(t('The two passwords provided do not match.'));
                         }
                     }
    +                $data['_allowPasswordUpdate'] = true;
                     $data['uPasswordConfirm'] = $passwordNew;
                     $data['uPassword'] = $passwordNew;
                 }
    @@ -415,6 +416,7 @@ public function save_account($uID = null)
                             }
                         }
                     }
    +                $data['_allowIgnoredIPMismatchesUpdate'] = true;
                     $data['ignoredIPMismatches'] = $ignoredIPMismatches;
                 }
     
    
  • concrete/controllers/single_page/download_file.php+11 5 modified
    @@ -218,15 +218,21 @@ public function submit_password($fID = 0)
     
                 $rcID = $this->post('rcID');
     
    -            if ($f->getPassword() == $this->post('password')) {
    -                if ($this->post('force')) {
    -                    return $this->force_download($f);
    +            if ($f->getPassword() === $this->post('password')) {
    +                $checker = new Checker($f);
    +                if ($checker->canViewFile()) {
    +                    if ($this->post('force')) {
    +                        return $this->force_download($f);
    +                    } else {
    +                        return $this->download($f);
    +                    }
                     } else {
    -                    return $this->download($f);
    +                    $this->set('error', t("You do not have permission to access this file."));
                     }
    +            } else {
    +                $this->set('error', t("Password incorrect. Please try again."));
                 }
     
    -            $this->set('error', t("Password incorrect. Please try again."));
                 $this->set('force', ($this->post('force') ? 1 : 0));
     
                 if ($fUUID !== null) {
    
  • concrete/elements/conversation/display.php+1 1 modified
    @@ -16,7 +16,7 @@
     }
     
     $editor = \Concrete\Core\Conversation\Editor\Editor::getActive();
    -$editor->setConversationObject($args['conversation']);
    +$editor->setConversationObject($conversation);
     
     $val = $app->make('token');
     $form = $app->make('helper/form');
    
  • concrete/js/ckeditor/concrete.js+1 1 modified
    @@ -1 +1 @@
    -(()=>{var e={461:()=>{CKEDITOR.plugins.add("concretefilemanager",{requires:"filebrowser",init:function(){CKEDITOR.on("dialogDefinition",function(e){var t=e.editor,n=e.data.definition,i=n.contents.length;function o(){return function(){t._.filebrowserSe=this;var e=this.getDialog();ConcreteFileManager.launchDialog(function(n){jQuery.fn.dialog.showLoader(),ConcreteFileManager.getFileDetails(n.fID,function(n){if(jQuery.fn.dialog.hideLoader(),n){var i=n.files[0];"image"!=e.getName()&&"image2"!=e.getName()||"info"!=e._.currentTabId?CKEDITOR.tools.callFunction(t._.filebrowserFn,i.urlDownload):CKEDITOR.tools.callFunction(t._.filebrowserFn,i.urlInline,function(){var t;e.dontResetSize=!0,(t=e.getContentElement("info","txtWidth"))&&t.setValue(""),(t=e.getContentElement("info","width"))&&t.setValue(""),(t=e.getContentElement("info","txtHeight"))&&t.setValue(""),(t=e.getContentElement("info","height"))&&t.setValue(""),(t=e.getContentElement("info","txtAlt"))&&t.setValue(i.title),(t=e.getContentElement("info","alt"))&&t.setValue(i.title)})}})})}}for(var a=0;a<i;a++){var r=n.contents[a].get("browse");null!==r&&(r.hidden=!1,r.onClick=o())}})}})},7068:()=>{CKEDITOR.plugins.add("concreteinline",{init:function(e){e.elementMode==CKEDITOR.ELEMENT_MODE_INLINE&&(e.addCommand("c5save",{exec:function(e){$("#"+e.element.$.id+"_content").val(e.getData()),ConcreteEvent.fire("EditModeBlockSaveInline"),e.destroy()}}),e.addCommand("c5cancel",{exec:function(e){ConcreteEvent.fire("EditModeExitInline"),e.destroy()}}),e.ui.addButton&&(e.ui.addButton("concrete_save",{label:e.lang.common.ok,command:"c5save",toolbar:"document,0"}),e.ui.addButton("concrete_cancel",{label:e.lang.common.cancel,command:"c5cancel",toolbar:"document,1"})))}})},3805:()=>{CKEDITOR.plugins.add("concretelink",{requires:"link",init:function(e){CKEDITOR.on("dialogDefinition",function(t){var n=t.data.name,i=t.data.definition,o=e.lang.common,a=function(e,t){t[e]||(t[e]={}),t[e][this.id]=this.getValue()||""},r=function(e){return a.call(this,"target",e)},l=function(){return CKEDITOR.plugins.link.getSelectedLink(t.editor)};if("link"==n){var s=i.getContents("info");null===s.get("sitemapBrowse")&&t.editor.config.sitemap&&s.add({type:"button",id:"sitemapBrowse",label:ccmi18n_editor.sitemap,title:ccmi18n_editor.sitemap,onClick:function(){jQuery.fn.dialog.open({width:"90%",height:"70%",modal:!1,title:ccmi18n_sitemap.choosePage,href:CCM_DISPATCHER_FILENAME+"/ccm/system/dialogs/page/sitemap_selector"}),ConcreteEvent.unsubscribe("SitemapSelectPage"),ConcreteEvent.subscribe("SitemapSelectPage",function(e,t){jQuery.fn.dialog.closeTop();var n=i.dialog.getContentElement("info","url");n&&n.setValue(CCM_APPLICATION_URL+"/index.php?cID="+t.cID)})}},"browse");var c=i.getContents("target");if(null!==c.get("linkTargetType")){var d=c.get("linkTargetType");"lightbox"!=d.items[3][1]&&(d.items.splice(3,0,["<lightbox>","lightbox"]),d.items.join()),null===c.get("lightboxFeatures")&&c.elements.push({type:"vbox",width:"100%",align:"center",padding:2,id:"lightboxFeatures",children:[{type:"fieldset",label:ccmi18n_editor.lightboxFeatures,children:[{type:"hbox",children:[{type:"checkbox",id:"imageLightbox",label:ccmi18n_editor.imageLink,setup:function(e){var t=l();null!==t&&void 0!==e.target&&("lightbox"==e.target.name&&"image"==t.data("concrete-link-lightbox")?this.setValue(1):this.setValue(0))},commit:r,onChange:function(e){this.getValue()?this.getDialog().getContentElement("target","lightboxDimensions").getElement().hide():this.getDialog().getContentElement("target","lightboxDimensions").getElement().show()}}]},{type:"hbox",id:"lightboxDimensions",children:[{type:"text",widths:["50%","50%"],labelLayout:"horizontal",label:o.width,id:"lightboxWidth",setup:function(e){var t=l();null!==t&&void 0!==e.target&&("lightbox"==e.target.name&&t.hasAttribute("data-concrete-link-lightbox-width")?this.setValue(t.data("concrete-link-lightbox-width")):this.setValue(null))},commit:r},{type:"text",labelLayout:"horizontal",widths:["50%","50%"],label:o.height,id:"lightboxHeight",setup:function(e){var t=l();null!==t&&void 0!==e.target&&("lightbox"==e.target.name&&t.hasAttribute("data-concrete-link-lightbox-height")?this.setValue(t.data("concrete-link-lightbox-height")):this.setValue(null))},commit:r}],setup:function(){this.getDialog().getContentElement("target","imageLightbox").getValue()?this.getElement().hide():this.getElement().show()}}]}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().hide(),"lightbox"!=this.getDialog().getContentElement("target","linkTargetType").getValue()&&this.getElement().hide()}}),d.onChange=CKEDITOR.tools.override(d.onChange,function(e){return function(){var t=this.getDialog().getContentElement("target","lightboxFeatures").getElement();"lightbox"!=this.getValue()||this._.selectedElement?t.hide():t.show(),e.call(this)}}),d.setup=function(e){e.target&&("lightbox"==e.target.name&&(e.target.type=e.target.name),this.setValue(e.target.type||"notSet")),this.onChange.call(this)},d.commit=function(e){e.target||(e.target={}),e.target.type=this.getValue()},i.onOk=CKEDITOR.tools.override(i.onOk,function(e){return function(){var t={},n={};this.commitContent(t),e.call(this);var i=l();null!==i&&("lightbox"==t.target.type?t.target.imageLightbox?(i.data("concrete-link-lightbox","image"),n={"data-concrete-link-lightbox-width":1,"data-concrete-link-lightbox-height":1}):(i.data("concrete-link-lightbox","iframe"),t.target.lightboxWidth&&t.target.lightboxHeight?(i.data("concrete-link-lightbox-width",t.target.lightboxWidth),i.data("concrete-link-lightbox-height",t.target.lightboxHeight)):n={"data-concrete-link-lightbox-width":1,"data-concrete-link-lightbox-height":1}):n={"data-concrete-link-lightbox":1,"data-concrete-link-lightbox-width":1,"data-concrete-link-lightbox-height":1},i.removeAttributes(n))}})}}})}})},1939:()=>{CKEDITOR.plugins.add("normalizeonchange",{init:function(e){CKEDITOR.on("instanceReady",function(e){e.editor.on("change",function(t){var n=e.editor.getSelection();if(n){var i=n.getStartElement();i&&i.$&&n.getStartElement().$.normalize()}})})}})},8315:()=>{CKEDITOR.plugins.add("concretestyles",{requires:["widget","stylescombo","menubutton"],init:function(e){},afterInit:function(e){var t=this;function n(e){return{exec:function(t){var n="> </",i=e.html,o=t.getSelection(),a=o&&o.getSelectedText();if(-1!=i.indexOf(n)&&a){for(var r=["address","article","aside","audio","blockquote","canvas","dd","div","dl","fieldset","figcaption","figure","figcaption","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","noscript","ol","output","p","pre","section","span","table","tfoot","ul","video"],l=o.getRanges(),s=new CKEDITOR.dom.element("div"),c=0,d=l.length;c<d;++c){var g=l[c],u=g.createBookmark2();s.append(g.cloneContents()),g.moveToBookmark(u),g.select()}for(var h=s.getHtml(),f=0;f<r.length;f++){var m=new RegExp("(<"+r[f]+"[^>]*>|</"+r[f]+">)","gi");h=h.replace(m,"")}i=i.replace(n,">"+h+"</")}t.insertHtml(i)}}}var i={name:"snippets",icon:"snippet.png",title:ccmi18n_editor.snippets,items:[]};if(e.config.snippets&&($.each(e.config.snippets,function(t,n){e.widgets.add(n.scsHandle,{template:n.scsName});var o={};o.name=n.scsHandle,o.icon="snippet.png",o.title=n.scsName,o.html='<span class="ccm-content-editor-snippet" contenteditable="false" data-scsHandle="'+n.scsHandle+'">'+n.scsName+"</span>",i.items.push(o)}),function(i){for(var o=i.items,a={},r=0;r<o.length;r++){var l=o[r],s=l.name;e.addCommand(s,n(l)),a[s]={label:l.title,command:s,group:i.name,role:"menuitem"}}e.addMenuGroup(i.name,1),e.addMenuItems(a),e.ui.add(i.name,CKEDITOR.UI_MENUBUTTON,{label:i.title,icon:t.path+"/icons/"+i.icon,toolbar:i.toolbar||"insert",onMenu:function(){var e={};for(var t in a)e[t]=CKEDITOR.TRISTATE_OFF;return e}})}(i,i.name)),e.config.classes){var o=[];$.each(e.config.classes,function(){var e={};e.name=this.title,void 0!==this.element?e.element=this.element:void 0!==this.forceBlock&&1==this.forceBlock?e.element=["h1","h2","h3","h4","h5","h6","p"]:e.element="span",void 0!==this.spanClass&&(e.attributes={class:this.spanClass}),void 0!==this.attributes&&(e.attributes=this.attributes),void 0!==this.styles&&(e.styles=this.styles),"widget"===this.type&&void 0!==this.widget&&(e.type="widget",e.widget=this.widget),o.push(e)}),e.fire("stylesSet",{styles:o})}}})},7320:()=>{CKEDITOR.plugins.add("concreteuploadimage",{requires:"uploadimage",init:function(e){e.on("fileUploadRequest",function(e){var t=e.data.fileLoader,n=t.xhr,i=new FormData;i.append("ccm_token",CCM_SECURITY_TOKEN),i.append("files[]",t.file,t.fileName),n.send(i),e.stop()}),e.on("fileUploadResponse",function(e){e.stop();var t=e.data,n=t.fileLoader.xhr;if(200==n.status){var i=jQuery.parseJSON(n.responseText);i.error?(t.message=i.errors.join(),e.cancel()):i.files.length>0&&(t.url=i.files[0].urlInline)}else t.message=n.responseText,e.cancel()})}})},8316:()=>{!function(e){if(void 0===e)throw Error("jQuery should be loaded before CKEditor jQuery adapter.");if("undefined"==typeof CKEDITOR)throw Error("CKEditor should be loaded before CKEditor jQuery adapter.");CKEDITOR.config.jqueryOverrideVal=void 0===CKEDITOR.config.jqueryOverrideVal||CKEDITOR.config.jqueryOverrideVal,e.extend(e.fn,{ckeditorGet:function(){var e=this.eq(0).data("ckeditorInstance");if(!e)throw"CKEditor is not initialized yet, use ckeditor() with a callback.";return e},ckeditor:function(t,n){if(!CKEDITOR.env.isCompatible)throw Error("The environment is incompatible.");if("function"!=typeof t){var i=n;n=t,t=i}var o=[];n=n||{},this.each(function(){var i=e(this),a=i.data("ckeditorInstance"),r=i.data("_ckeditorInstanceLock"),l=this,s=new e.Deferred;o.push(s.promise()),a&&!r?(t&&t.apply(a,[this]),s.resolve()):r?a.once("instanceReady",function(){setTimeout(function e(){a.element?(a.element.$==l&&t&&t.apply(a,[l]),s.resolve()):setTimeout(e,100)},0)},null,null,9999):((n.autoUpdateElement||void 0===n.autoUpdateElement&&CKEDITOR.config.autoUpdateElement)&&(n.autoUpdateElementJquery=!0),n.autoUpdateElement=!1,i.data("_ckeditorInstanceLock",!0),a=e(this).is("textarea")?CKEDITOR.replace(l,n):CKEDITOR.inline(l,n),i.data("ckeditorInstance",a),a.on("instanceReady",function(n){var o=n.editor;setTimeout(function a(){if(o.element){if(n.removeListener(),o.on("dataReady",function(){i.trigger("dataReady.ckeditor",[o])}),o.on("setData",function(e){i.trigger("setData.ckeditor",[o,e.data])}),o.on("getData",function(e){i.trigger("getData.ckeditor",[o,e.data])},999),o.on("destroy",function(){i.trigger("destroy.ckeditor",[o])}),o.on("save",function(){return e(l.form).trigger("submit"),!1},null,null,20),o.config.autoUpdateElementJquery&&i.is("textarea")&&e(l.form).length){var r=function(){i.ckeditor(function(){o.updateElement()})};e(l.form).on("submit",r),e(l.form).on("form-pre-serialize",r),i.on("destroy.ckeditor",function(){e(l.form).off("submit",r),e(l.form).off("form-pre-serialize",r)})}o.on("destroy",function(){i.removeData("ckeditorInstance")}),i.removeData("_ckeditorInstanceLock"),i.trigger("instanceReady.ckeditor",[o]),t&&t.apply(o,[l]),s.resolve()}else setTimeout(a,100)},0)},null,null,9999))});var a=new e.Deferred;return this.promise=a.promise(),e.when.apply(this,o).then(function(){a.resolve()}),this.editor=this.eq(0).data("ckeditorInstance"),this}}),CKEDITOR.config.jqueryOverrideVal&&(e.fn.val=CKEDITOR.tools.override(e.fn.val,function(t){return function(n){if(arguments.length){var i=this,o=[],a=this.each(function(){var i=e(this),a=i.data("ckeditorInstance");if(i.is("textarea")&&a){var r=new e.Deferred;return a.setData(n,function(){r.resolve()}),o.push(r.promise()),!0}return t.call(i,n)});if(o.length){var r=new e.Deferred;return e.when.apply(this,o).done(function(){r.resolveWith(i)}),r.promise()}return a}var l=(a=e(this).eq(0)).data("ckeditorInstance");return a.is("textarea")&&l?l.getData():t.call(a)}}))}(window.jQuery)}},t={};function n(i){var o=t[i];if(void 0!==o)return o.exports;var a=t[i]={exports:{}};return e[i](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var i in t)n.o(t,i)&&!n.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";n(8316),n(1939),n(7068),n(3805),n(461),n(8315),n(7320)})()})();
    \ No newline at end of file
    +(()=>{var e={461:()=>{CKEDITOR.plugins.add("concretefilemanager",{requires:"filebrowser",init:function(){CKEDITOR.on("dialogDefinition",function(e){var t=e.editor,n=e.data.definition,i=n.contents.length;function o(){return function(){t._.filebrowserSe=this;var e=this.getDialog();ConcreteFileManager.launchDialog(function(n){jQuery.fn.dialog.showLoader(),ConcreteFileManager.getFileDetails(n.fID,function(n){if(jQuery.fn.dialog.hideLoader(),n){var i=n.files[0];"image"!=e.getName()&&"image2"!=e.getName()||"info"!=e._.currentTabId?CKEDITOR.tools.callFunction(t._.filebrowserFn,i.urlDownload):CKEDITOR.tools.callFunction(t._.filebrowserFn,i.urlInline,function(){var t;e.dontResetSize=!0,(t=e.getContentElement("info","txtWidth"))&&t.setValue(""),(t=e.getContentElement("info","width"))&&t.setValue(""),(t=e.getContentElement("info","txtHeight"))&&t.setValue(""),(t=e.getContentElement("info","height"))&&t.setValue(""),(t=e.getContentElement("info","txtAlt"))&&t.setValue(i.title),(t=e.getContentElement("info","alt"))&&t.setValue(i.title)})}})})}}for(var a=0;a<i;a++){var r=n.contents[a].get("browse");null!==r&&(r.hidden=!1,r.onClick=o(),"undefined"!=typeof ccmi18n_editor&&ccmi18n_editor.selectFile&&(r.label=ccmi18n_editor.selectFile,r.title=ccmi18n_editor.selectFile))}})}})},7068:()=>{CKEDITOR.plugins.add("concreteinline",{init:function(e){e.elementMode==CKEDITOR.ELEMENT_MODE_INLINE&&(e.addCommand("c5save",{exec:function(e){$("#"+e.element.$.id+"_content").val(e.getData()),ConcreteEvent.fire("EditModeBlockSaveInline"),e.destroy()}}),e.addCommand("c5cancel",{exec:function(e){ConcreteEvent.fire("EditModeExitInline"),e.destroy()}}),e.ui.addButton&&(e.ui.addButton("concrete_save",{label:e.lang.common.ok,command:"c5save",toolbar:"document,0"}),e.ui.addButton("concrete_cancel",{label:e.lang.common.cancel,command:"c5cancel",toolbar:"document,1"})))}})},3805:()=>{CKEDITOR.plugins.add("concretelink",{requires:"link",init:function(e){CKEDITOR.on("dialogDefinition",function(t){var n=t.data.name,i=t.data.definition,o=e.lang.common,a=function(e,t){t[e]||(t[e]={}),t[e][this.id]=this.getValue()||""},r=function(e){return a.call(this,"target",e)},l=function(){return CKEDITOR.plugins.link.getSelectedLink(t.editor)};if("link"==n){var s=i.getContents("info");null===s.get("sitemapBrowse")&&t.editor.config.sitemap&&s.add({type:"button",id:"sitemapBrowse",label:ccmi18n_editor.sitemap,title:ccmi18n_editor.sitemap,onClick:function(){jQuery.fn.dialog.open({width:"90%",height:"70%",modal:!1,title:ccmi18n_sitemap.choosePage,href:CCM_DISPATCHER_FILENAME+"/ccm/system/dialogs/page/sitemap_selector"}),ConcreteEvent.unsubscribe("SitemapSelectPage"),ConcreteEvent.subscribe("SitemapSelectPage",function(e,t){jQuery.fn.dialog.closeTop();var n=i.dialog.getContentElement("info","url");n&&n.setValue(CCM_APPLICATION_URL+"/index.php?cID="+t.cID)})}},"browse");var c=i.getContents("target");if(null!==c.get("linkTargetType")){var d=c.get("linkTargetType");"lightbox"!=d.items[3][1]&&(d.items.splice(3,0,["<lightbox>","lightbox"]),d.items.join()),null===c.get("lightboxFeatures")&&c.elements.push({type:"vbox",width:"100%",align:"center",padding:2,id:"lightboxFeatures",children:[{type:"fieldset",label:ccmi18n_editor.lightboxFeatures,children:[{type:"hbox",children:[{type:"checkbox",id:"imageLightbox",label:ccmi18n_editor.imageLink,setup:function(e){var t=l();null!==t&&void 0!==e.target&&("lightbox"==e.target.name&&"image"==t.data("concrete-link-lightbox")?this.setValue(1):this.setValue(0))},commit:r,onChange:function(e){this.getValue()?this.getDialog().getContentElement("target","lightboxDimensions").getElement().hide():this.getDialog().getContentElement("target","lightboxDimensions").getElement().show()}}]},{type:"hbox",id:"lightboxDimensions",children:[{type:"text",widths:["50%","50%"],labelLayout:"horizontal",label:o.width,id:"lightboxWidth",setup:function(e){var t=l();null!==t&&void 0!==e.target&&("lightbox"==e.target.name&&t.hasAttribute("data-concrete-link-lightbox-width")?this.setValue(t.data("concrete-link-lightbox-width")):this.setValue(null))},commit:r},{type:"text",labelLayout:"horizontal",widths:["50%","50%"],label:o.height,id:"lightboxHeight",setup:function(e){var t=l();null!==t&&void 0!==e.target&&("lightbox"==e.target.name&&t.hasAttribute("data-concrete-link-lightbox-height")?this.setValue(t.data("concrete-link-lightbox-height")):this.setValue(null))},commit:r}],setup:function(){this.getDialog().getContentElement("target","imageLightbox").getValue()?this.getElement().hide():this.getElement().show()}}]}],setup:function(){this.getDialog().getContentElement("info","linkType")||this.getElement().hide(),"lightbox"!=this.getDialog().getContentElement("target","linkTargetType").getValue()&&this.getElement().hide()}}),d.onChange=CKEDITOR.tools.override(d.onChange,function(e){return function(){var t=this.getDialog().getContentElement("target","lightboxFeatures").getElement();"lightbox"!=this.getValue()||this._.selectedElement?t.hide():t.show(),e.call(this)}}),d.setup=function(e){e.target&&("lightbox"==e.target.name&&(e.target.type=e.target.name),this.setValue(e.target.type||"notSet")),this.onChange.call(this)},d.commit=function(e){e.target||(e.target={}),e.target.type=this.getValue()},i.onOk=CKEDITOR.tools.override(i.onOk,function(e){return function(){var t={},n={};this.commitContent(t),e.call(this);var i=l();null!==i&&("lightbox"==t.target.type?t.target.imageLightbox?(i.data("concrete-link-lightbox","image"),n={"data-concrete-link-lightbox-width":1,"data-concrete-link-lightbox-height":1}):(i.data("concrete-link-lightbox","iframe"),t.target.lightboxWidth&&t.target.lightboxHeight?(i.data("concrete-link-lightbox-width",t.target.lightboxWidth),i.data("concrete-link-lightbox-height",t.target.lightboxHeight)):n={"data-concrete-link-lightbox-width":1,"data-concrete-link-lightbox-height":1}):n={"data-concrete-link-lightbox":1,"data-concrete-link-lightbox-width":1,"data-concrete-link-lightbox-height":1},i.removeAttributes(n))}})}}})}})},1939:()=>{CKEDITOR.plugins.add("normalizeonchange",{init:function(e){CKEDITOR.on("instanceReady",function(e){e.editor.on("change",function(t){var n=e.editor.getSelection();if(n){var i=n.getStartElement();i&&i.$&&n.getStartElement().$.normalize()}})})}})},8315:()=>{CKEDITOR.plugins.add("concretestyles",{requires:["widget","stylescombo","menubutton"],init:function(e){},afterInit:function(e){var t=this;function n(e){return{exec:function(t){var n="> </",i=e.html,o=t.getSelection(),a=o&&o.getSelectedText();if(-1!=i.indexOf(n)&&a){for(var r=["address","article","aside","audio","blockquote","canvas","dd","div","dl","fieldset","figcaption","figure","figcaption","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","noscript","ol","output","p","pre","section","span","table","tfoot","ul","video"],l=o.getRanges(),s=new CKEDITOR.dom.element("div"),c=0,d=l.length;c<d;++c){var g=l[c],u=g.createBookmark2();s.append(g.cloneContents()),g.moveToBookmark(u),g.select()}for(var h=s.getHtml(),m=0;m<r.length;m++){var f=new RegExp("(<"+r[m]+"[^>]*>|</"+r[m]+">)","gi");h=h.replace(f,"")}i=i.replace(n,">"+h+"</")}t.insertHtml(i)}}}var i={name:"snippets",icon:"snippet.png",title:ccmi18n_editor.snippets,items:[]};if(e.config.snippets&&($.each(e.config.snippets,function(t,n){e.widgets.add(n.scsHandle,{template:n.scsName});var o={};o.name=n.scsHandle,o.icon="snippet.png",o.title=n.scsName,o.html='<span class="ccm-content-editor-snippet" contenteditable="false" data-scsHandle="'+n.scsHandle+'">'+n.scsName+"</span>",i.items.push(o)}),function(i){for(var o=i.items,a={},r=0;r<o.length;r++){var l=o[r],s=l.name;e.addCommand(s,n(l)),a[s]={label:l.title,command:s,group:i.name,role:"menuitem"}}e.addMenuGroup(i.name,1),e.addMenuItems(a),e.ui.add(i.name,CKEDITOR.UI_MENUBUTTON,{label:i.title,icon:t.path+"/icons/"+i.icon,toolbar:i.toolbar||"insert",onMenu:function(){var e={};for(var t in a)e[t]=CKEDITOR.TRISTATE_OFF;return e}})}(i,i.name)),e.config.classes){var o=[];$.each(e.config.classes,function(){var e={};e.name=this.title,void 0!==this.element?e.element=this.element:void 0!==this.forceBlock&&1==this.forceBlock?e.element=["h1","h2","h3","h4","h5","h6","p"]:e.element="span",void 0!==this.spanClass&&(e.attributes={class:this.spanClass}),void 0!==this.attributes&&(e.attributes=this.attributes),void 0!==this.styles&&(e.styles=this.styles),"widget"===this.type&&void 0!==this.widget&&(e.type="widget",e.widget=this.widget),o.push(e)}),e.fire("stylesSet",{styles:o})}}})},7320:()=>{CKEDITOR.plugins.add("concreteuploadimage",{requires:"uploadimage",init:function(e){e.on("fileUploadRequest",function(e){var t=e.data.fileLoader,n=t.xhr,i=new FormData;i.append("ccm_token",CCM_SECURITY_TOKEN),i.append("files[]",t.file,t.fileName),n.send(i),e.stop()}),e.on("fileUploadResponse",function(e){e.stop();var t=e.data,n=t.fileLoader.xhr;if(200==n.status){var i=jQuery.parseJSON(n.responseText);i.error?(t.message=i.errors.join(),e.cancel()):i.files.length>0&&(t.url=i.files[0].urlInline)}else t.message=n.responseText,e.cancel()})}})},8316:()=>{!function(e){if(void 0===e)throw Error("jQuery should be loaded before CKEditor jQuery adapter.");if("undefined"==typeof CKEDITOR)throw Error("CKEditor should be loaded before CKEditor jQuery adapter.");CKEDITOR.config.jqueryOverrideVal=void 0===CKEDITOR.config.jqueryOverrideVal||CKEDITOR.config.jqueryOverrideVal,e.extend(e.fn,{ckeditorGet:function(){var e=this.eq(0).data("ckeditorInstance");if(!e)throw"CKEditor is not initialized yet, use ckeditor() with a callback.";return e},ckeditor:function(t,n){if(!CKEDITOR.env.isCompatible)throw Error("The environment is incompatible.");if("function"!=typeof t){var i=n;n=t,t=i}var o=[];n=n||{},this.each(function(){var i=e(this),a=i.data("ckeditorInstance"),r=i.data("_ckeditorInstanceLock"),l=this,s=new e.Deferred;o.push(s.promise()),a&&!r?(t&&t.apply(a,[this]),s.resolve()):r?a.once("instanceReady",function(){setTimeout(function e(){a.element?(a.element.$==l&&t&&t.apply(a,[l]),s.resolve()):setTimeout(e,100)},0)},null,null,9999):((n.autoUpdateElement||void 0===n.autoUpdateElement&&CKEDITOR.config.autoUpdateElement)&&(n.autoUpdateElementJquery=!0),n.autoUpdateElement=!1,i.data("_ckeditorInstanceLock",!0),a=e(this).is("textarea")?CKEDITOR.replace(l,n):CKEDITOR.inline(l,n),i.data("ckeditorInstance",a),a.on("instanceReady",function(n){var o=n.editor;setTimeout(function a(){if(o.element){if(n.removeListener(),o.on("dataReady",function(){i.trigger("dataReady.ckeditor",[o])}),o.on("setData",function(e){i.trigger("setData.ckeditor",[o,e.data])}),o.on("getData",function(e){i.trigger("getData.ckeditor",[o,e.data])},999),o.on("destroy",function(){i.trigger("destroy.ckeditor",[o])}),o.on("save",function(){return e(l.form).trigger("submit"),!1},null,null,20),o.config.autoUpdateElementJquery&&i.is("textarea")&&e(l.form).length){var r=function(){i.ckeditor(function(){o.updateElement()})};e(l.form).on("submit",r),e(l.form).on("form-pre-serialize",r),i.on("destroy.ckeditor",function(){e(l.form).off("submit",r),e(l.form).off("form-pre-serialize",r)})}o.on("destroy",function(){i.removeData("ckeditorInstance")}),i.removeData("_ckeditorInstanceLock"),i.trigger("instanceReady.ckeditor",[o]),t&&t.apply(o,[l]),s.resolve()}else setTimeout(a,100)},0)},null,null,9999))});var a=new e.Deferred;return this.promise=a.promise(),e.when.apply(this,o).then(function(){a.resolve()}),this.editor=this.eq(0).data("ckeditorInstance"),this}}),CKEDITOR.config.jqueryOverrideVal&&(e.fn.val=CKEDITOR.tools.override(e.fn.val,function(t){return function(n){if(arguments.length){var i=this,o=[],a=this.each(function(){var i=e(this),a=i.data("ckeditorInstance");if(i.is("textarea")&&a){var r=new e.Deferred;return a.setData(n,function(){r.resolve()}),o.push(r.promise()),!0}return t.call(i,n)});if(o.length){var r=new e.Deferred;return e.when.apply(this,o).done(function(){r.resolveWith(i)}),r.promise()}return a}var l=(a=e(this).eq(0)).data("ckeditorInstance");return a.is("textarea")&&l?l.getData():t.call(a)}}))}(window.jQuery)}},t={};function n(i){var o=t[i];if(void 0!==o)return o.exports;var a=t[i]={exports:{}};return e[i](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var i in t)n.o(t,i)&&!n.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";n(8316),n(1939),n(7068),n(3805),n(461),n(8315),n(7320)})()})();
    \ No newline at end of file
    
  • concrete/js/cms.js+1 1 modified
  • concrete/routes/dialogs/files.php+0 1 modified
    @@ -43,7 +43,6 @@
     $router->all('/thumbnails', 'Thumbnails::view');
     $router->all('/thumbnails/edit', 'Thumbnails\Edit::view');
     $router->all('/thumbnails/edit/submit', 'Thumbnails\Edit::submit');
    -$router->all('/usage/{fID}', 'Usage::view');
     $router->all('/statistics/{fID}', 'Statistics::view');
     $router->all('/statistics/load_more/{fID}', 'Statistics::load_more');
     $router->all('/statistics/download/{fID}', 'Statistics::download');
    
  • concrete/single_pages/dashboard/extend/install.php+8 2 modified
    @@ -315,7 +315,10 @@
                                 <p><?= h($pb->summary); ?></p>
                             </div>
                             <div class="d-block ms-auto">
    -                            <a href="<?= URL::to('/dashboard/extend/install', 'download', $pb->id); ?>" class="btn btn-sm btn-secondary"><?= t('Download'); ?></a>
    +                            <form method="post" action="<?= URL::to('/dashboard/extend/install', 'download', $pb->id); ?>" class="d-inline">
    +                                <?= $token->output('download_package'); ?>
    +                                <button type="submit" class="btn btn-sm btn-secondary"><?= t('Download'); ?></button>
    +                            </form>
                             </div>
                         </div>
                         <?php
    @@ -338,7 +341,10 @@
                             } else {
                                 ?>
                                 <div class="btn-group ms-auto d-block">
    -                                <a href="<?= URL::to('/dashboard/extend/install', 'install_package', $obj->getPackageHandle()); ?>" class="btn btn-sm btn-secondary"><?= t('Install'); ?></a><?php
    +                                <form method="post" action="<?= URL::to('/dashboard/extend/install', 'install_package', $obj->getPackageHandle()); ?>" class="d-inline">
    +                                    <?= $token->output('install_package'); ?>
    +                                    <button type="submit" class="btn btn-sm btn-secondary"><?= t('Install'); ?></button>
    +                                </form><?php
                                     if ($displayDeleteBtn) {
                                         ?><a href="javascript:void(0)" class="btn btn-sm btn-danger" onclick="deletePackage('<?= $obj->getPackageHandle() ?>', '<?= $obj->getPackageName() ?>')"><?= t('Delete') ?></a>
                                         <?php
    
  • concrete/single_pages/dashboard/extend/update.php+10 2 modified
    @@ -60,7 +60,10 @@
                             ?>
     						<td class="ccm-marketplace-list-install-button">
                                 <a class="btn" target="_blank" href="#"><?=t('More Information')?></a>
    -                            <?=$ch->button(t('Download and Install'), View::url('/dashboard/extend/update', 'prepare_remote_upgrade', $remotePackage->id), '', 'primary')?>
    +                            <form method="post" action="<?= View::url('/dashboard/extend/update', 'prepare_remote_upgrade', $remotePackage->id) ?>" class="d-inline">
    +                                <?= $valt->output('prepare_remote_upgrade') ?>
    +                                <button type="submit" class="btn btn-secondary primary"><?= t('Download and Install') ?></button>
    +                            </form>
                             </td>
     					    <?php
                         }
    @@ -107,7 +110,12 @@
     					<td class="ccm-addon-list-description"><h3><?=$pkg->getPackageName()?></h3><p><?=$pkg->getPackageDescription()?></p>
     					<p><strong><?=t('New Version: %s. Upgrading from: %s.', $pkg->getPackageVersion(), $entity->getPackageVersion())?></strong></p>
     					</td>
    -					<td class="ccm-marketplace-list-install-button"><?=$ch->button(t('Update Add-On'), View::url('/dashboard/extend/update', 'do_update', $pkg->getPackageHandle()), '', 'btn-primary')?></td>
    +					<td class="ccm-marketplace-list-install-button">
    +                        <form method="post" action="<?= View::url('/dashboard/extend/update', 'do_update', $pkg->getPackageHandle()) ?>" class="d-inline">
    +                            <?= $valt->output('update_addon') ?>
    +                            <button type="submit" class="btn btn-secondary btn-primary"><?= t('Update Add-On') ?></button>
    +                        </form>
    +                    </td>
     				</tr>
     				<tr>
     					<td colspan="2" style="border-top: 0px">
    
  • concrete/src/Api/OAuth/Controller.php+57 29 modified
    @@ -14,6 +14,11 @@
     use Concrete\Core\Logging\LoggerAwareInterface;
     use Concrete\Core\Logging\LoggerAwareTrait;
     use Concrete\Core\Support\Facade\Application;
    +use Concrete\Core\User\Exception\FailedLoginThresholdExceededException;
    +use Concrete\Core\User\Exception\InvalidCredentialsException;
    +use Concrete\Core\User\Exception\UserDeactivatedException;
    +use Concrete\Core\User\Exception\UserException;
    +use Concrete\Core\User\Login\LoginService;
     use Concrete\Core\User\User as UserObject;
     use Concrete\Core\Validation\CSRF\Token;
     use Concrete\Core\View\View;
    @@ -193,6 +198,8 @@ public function handleLogin(AuthorizationRequest $request)
         {
             $error = new ErrorList();
             $emailLogin = $this->config->get('concrete.user.registration.email_registration');
    +        $app = Application::getFacadeApplication();
    +        $loginService = $app->make(LoginService::class);
     
             while ($this->request->getMethod() === 'POST') {
     
    @@ -204,43 +211,47 @@ public function handleLogin(AuthorizationRequest $request)
                 $query = $this->request->getParsedBody();
                 $user = array_get($query, 'uName');
                 $password = array_get($query, 'uPassword');
    +            $user = is_string($user) ? trim($user) : '';
    +            $password = is_string($password) ? $password : '';
     
    -            $userRepository = $this->entityManager->getRepository(User::class);
    -
    -            /** @var User $user */
    -            $user = $userRepository->findOneBy([$emailLogin ? 'uEmail' : 'uName' => $user]);
    -
    -            $app = Application::getFacadeApplication();
    -            $hasher = $app->make(\Concrete\Core\Encryption\PasswordHasher::class);
    -
    -            // User successfully logged in
    -            if ($user && $user->getUserID() && $hasher->checkPassword($password, $user->getUserPassword())) {
    -                if ($hasher->needsRehash($user->getUserPassword())) {
    -                    $em = $app->make(EntityManagerInterface::class);
    -
    -                    try {
    -                        $em->transactional(function () use ($user, $hasher, $password) {
    -                            $user->setUserPassword($hasher->hashPassword($password));
    -                        });
    -                    } catch (\Throwable $e) {
    -                        $this->logger->emergency('Unable to rehash password for user {user} ({id}): {message}', [
    -                            'user' => $user->getUserName(),
    -                            'id' => $user->getUserID(),
    -                            'message' => $e->getMessage(),
    -                        ]);
    -                    }
    +            if ($user === '' || $password === '') {
    +                if ($emailLogin) {
    +                    $error->add(t('Please provide both email address and password.'));
    +                } else {
    +                    $error->add(t('Please provide both username and password.'));
                     }
    +                break;
    +            }
    +
    +            $failedLogins = $app->make('failed_login');
    +            if ($failedLogins->isDenylisted()) {
    +                $error->add($failedLogins->getErrorMessage());
    +                break;
    +            }
    +
    +            try {
    +                $loggedInUser = $loginService->login($user, $password);
    +                $loginService->logLoginAttempt($user);
     
    -                $userInfo = $this->entityManager->find(User::class, $user->getUserID());
    +                $userInfo = $this->entityManager->find(User::class, $loggedInUser->getUserID());
                     $request->setUser($userInfo);
                     $this->storeRequest($request);
     
                     return new \RedirectResponse($this->request->getUri());
    -            } else {
    -                if ($this->config->get('concrete.user.registration.email_registration')) {
    -                    $message = t('Invalid email address or password.');
    +            } catch (UserException $e) {
    +                try {
    +                    $this->handleFailedLogin($loginService, $user, $password, $e);
    +                } catch (UserException $x) {
    +                    $e = $x;
    +                }
    +                if ($e instanceof InvalidCredentialsException) {
    +                    if ($emailLogin) {
    +                        $message = t('Invalid email address or password.');
    +                    } else {
    +                        $message = t('Invalid username or password.');
    +                    }
                     } else {
    -                    $message = t('Invalid username or password.');
    +                    $message = $e->getMessage();
                     }
                     $error->add($message);
                 }
    @@ -260,6 +271,23 @@ public function handleLogin(AuthorizationRequest $request)
             return new \Concrete\Core\Http\Response($contents->render());
         }
     
    +    protected function handleFailedLogin(LoginService $loginService, $uName, $uPassword, UserException $e)
    +    {
    +        if ($e instanceof InvalidCredentialsException) {
    +            try {
    +                $loginService->failLogin($uName, $uPassword);
    +            } catch (FailedLoginThresholdExceededException $e) {
    +                $loginService->logLoginAttempt($uName, ['Failed Login Threshold Exceeded', $e->getMessage()]);
    +                throw $e;
    +            } catch (UserDeactivatedException $e) {
    +                $loginService->logLoginAttempt($uName, ['User Deactivated', $e->getMessage()]);
    +                throw $e;
    +            }
    +        }
    +
    +        $loginService->logLoginAttempt($uName, ['Invalid Credentials', $e->getMessage()]);
    +    }
    +
         /**
          * Handle the scope authorization portion of an authorization request
          * This method renders a view that outputs a list of scopes and asks the user to verify that they want to give the
    
  • concrete/src/Block/BlockController.php+4 1 modified
    @@ -4,6 +4,7 @@
     use Concrete\Core\Area\Area;
     use Concrete\Core\Backup\ContentExporter;
     use Concrete\Core\Backup\ContentImporter;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Block\View\BlockViewTemplate;
     use Concrete\Core\Database\Connection\Connection;
     use Concrete\Core\Editor\LinkAbstractor;
    @@ -155,6 +156,8 @@ class BlockController extends \Concrete\Core\Controller\AbstractController
          */
         public static $btTitleFormats = ['h1' => 'H1', 'h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'h5' => 'H5', 'h6' => 'H6', 'p' => 'Normal'];
     
    +    public $saveMode = SaveMode::SAVE_MODE_REQUEST;
    +
         /**
          * Set this to true if the data sent to the save/performSave methods can contain NULL values that should be persisted.
          *
    @@ -540,7 +543,7 @@ public function import($page, $arHandle, \SimpleXMLElement $blockNode)
             $blockData = [];
     
             $bt = \Concrete\Core\Block\BlockType\BlockType::getByHandle($this->btHandle);
    -        $b = $page->addBlock($bt, $arHandle, $args);
    +        $b = $page->addBlock($bt, $arHandle, $args, SaveMode::SAVE_MODE_IMPORT);
             $bName = (string) $blockNode['name'];
             $bFilename = (string) $blockNode['custom-template'];
             if ($bName) {
    
  • concrete/src/Block/Controller/SaveMode.php+9 0 added
    @@ -0,0 +1,9 @@
    +<?php
    +
    +namespace Concrete\Core\Block\Controller;
    +
    +class SaveMode
    +{
    +    public const SAVE_MODE_REQUEST = 'request';
    +    public const SAVE_MODE_IMPORT = 'import';
    +}
    
  • concrete/src/Conversation/FrontendController.php+4 0 modified
    @@ -260,6 +260,10 @@ protected function getBlockConversation(): Conversation
                 if (!($blockConversation instanceof Conversation)) {
                     throw new UserMessageException(t('Invalid Conversation.'));
                 }
    +            $cp = new Checker($blockConversation);
    +            if (!$cp->canViewConversation()) {
    +                throw new UserMessageException(t('Access Denied.'));
    +            }
                 $this->blockConversation = $blockConversation;
             }
     
    
  • concrete/src/Entity/Block/BlockType/BlockType.php+4 2 modified
    @@ -5,6 +5,7 @@
     use BlockTypeSet;
     use Concrete\Block\CoreStackDisplay\Controller;
     use Concrete\Core\Block\BlockType\BlockTypeList;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Block\View\BlockView;
     use Concrete\Core\Database\Schema\Schema;
     use Concrete\Core\Filesystem\TemplateFile;
    @@ -630,10 +631,10 @@ public function delete()
          * @param mixed            $data
          * @param bool|\Collection $c
          * @param bool|\Area       $a
    -     *
    +     * @param string $saveMode
          * @return bool|\Concrete\Core\Block\Block
          */
    -    public function add($data, $c = false, $a = false)
    +    public function add($data, $c = false, $a = false, ?string $saveMode = SaveMode::SAVE_MODE_REQUEST)
         {
             $app = Facade::getFacadeApplication();
             $db = $app->make('database')->connection();
    @@ -672,6 +673,7 @@ public function add($data, $c = false, $a = false)
                 }
                 $class = $this->getBlockTypeClass();
                 $bc = $app->make($class, ['obj' => $nb]);
    +            $bc->saveMode = $saveMode;
                 $bc->save($data);
     
                 return Block::getByID($bIDnew);
    
  • concrete/src/Feed/FeedService.php+30 2 modified
    @@ -3,7 +3,12 @@
     
     use Concrete\Core\Cache\Adapter\LaminasCacheDriver;
     use Concrete\Core\Config\Repository\Repository;
    +use Concrete\Core\Error\UserMessageException;
     use Concrete\Core\Http\Client\Factory as HttpClientFactory;
    +use Concrete\Core\Url\Validation\InvalidRemoteUrlException;
    +use Concrete\Core\Url\Validation\RemoteUrlRequestOptionsBuilder;
    +use Concrete\Core\Url\Validation\RemoteUrlValidator;
    +use Concrete\Core\Url\Validation\ValidatedRemoteUrl;
     use Laminas\Feed\Reader\Feed\FeedInterface;
     use Laminas\Feed\Reader\Reader;
     
    @@ -19,10 +24,22 @@ class FeedService
          */
         protected $httpClientFactory;
     
    +    /**
    +     * @var \Concrete\Core\Url\Validation\RemoteUrlValidator
    +     */
    +    protected $remoteUrlValidator;
    +
    +    /**
    +     * @var \Concrete\Core\Url\Validation\RemoteUrlRequestOptionsBuilder
    +     */
    +    protected $remoteUrlRequestOptionsBuilder;
    +
         public function __construct(Repository $config, HttpClientFactory $httpClientFactory)
         {
             $this->config = $config;
             $this->httpClientFactory = $httpClientFactory;
    +        $this->remoteUrlValidator = new RemoteUrlValidator();
    +        $this->remoteUrlRequestOptionsBuilder = new RemoteUrlRequestOptionsBuilder();
         }
     
         /**
    @@ -38,7 +55,8 @@ public function load($url, $cache = 3600)
                 Reader::setCache(new LaminasCacheDriver('cache/expensive', $cache));
             }
     
    -        Reader::setHttpClient(new GuzzleClient($this->buildHttpClient()));
    +        $validatedUrl = $this->getValidatedFeedUrl($url);
    +        Reader::setHttpClient(new GuzzleClient($this->buildHttpClient($validatedUrl)));
     
             // Load the RSS feed, either from remote URL or from cache
             // (if specified above and still fresh)
    @@ -59,12 +77,22 @@ public function getPosts(FeedInterface $feed): array
         /**
          * @return \Concrete\Core\Http\Client\Client
          */
    -    protected function buildHttpClient()
    +    protected function buildHttpClient(ValidatedRemoteUrl $validatedUrl)
         {
             $options = [
                 'timeout' => 5,
             ] + $this->httpClientFactory->getDefaultOptions($this->config);
    +        $options = array_replace($options, $this->remoteUrlRequestOptionsBuilder->build($validatedUrl));
     
             return $this->httpClientFactory->createFromOptions($options);
         }
    +
    +    protected function getValidatedFeedUrl($url)
    +    {
    +        try {
    +            return $this->remoteUrlValidator->validate((string) $url);
    +        } catch (InvalidRemoteUrlException $x) {
    +            throw new UserMessageException(t('The RSS feed URL is not valid.'));
    +        }
    +    }
     }
    
  • concrete/src/Legacy/Pagination.php+3 0 modified
    @@ -194,6 +194,7 @@ public function getNext($linkText = false, $wrapper = 'span')
                 return '<' . $wrapper . ' class="page-link"' . ($wrapper == 'a' ? ' href="#"' : '') . '>'.$linkText.'</' . $wrapper . '>';
             } else {
                 $linkURL = str_replace("%pageNum%", $this->getNextInt() + 1, $this->URL);
    +            $linkURL = htmlspecialchars($linkURL, ENT_QUOTES, 'UTF-8');
     
                 return '<a class="page-link" href="'.$linkURL.'" '.$this->getJSFunctionCall($this->getNextInt() + 1).'>'.$linkText.'</a>';
             }
    @@ -221,6 +222,7 @@ public function getPrevious($linkText = false, $wrapper = 'span')
                 return '<' . $wrapper . ' class="page-link"' . ($wrapper == 'a' ? ' href="#"' : '') . '>'.$linkText.'</' . $wrapper . '>';
             } else {
                 $linkURL = str_replace("%pageNum%", $this->getPreviousInt() + 1, $this->URL);
    +            $linkURL = htmlspecialchars($linkURL, ENT_QUOTES, 'UTF-8');
     
                 return '<a class="page-link" href="'.$linkURL.'" '.$this->getJSFunctionCall($this->getPreviousInt() + 1).'>'.$linkText.'</a>';
             }
    @@ -301,6 +303,7 @@ public function getPages($wrapper = 'span')
                     }
                 } else {
                     $linkURL = str_replace("%pageNum%", $i + 1, $this->URL);
    +                $linkURL = htmlspecialchars($linkURL, ENT_QUOTES, 'UTF-8');
     
                     if ($wrapper == 'li') {
                         $pages .= "<li class=\"page-item\"><a class=\"page-link\" href=\"{$linkURL}\" ".$this->getJSFunctionCall($i + 1).">".($i + 1)."</a></li>";
    
  • concrete/src/Page/Collection/Collection.php+3 2 modified
    @@ -11,6 +11,7 @@
     use Concrete\Core\Area\CustomStyle as AreaCustomStyle;
     use Concrete\Core\Area\GlobalArea;
     use Concrete\Core\Attribute\Key\CollectionKey;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Block\CustomStyle as BlockCustomStyle;
     use Concrete\Core\Block\CustomStyleRepository as BlockCustomStyleRepository;
     use Concrete\Core\Database\Connection\Connection;
    @@ -1010,14 +1011,14 @@ public function getBlockIDs($arHandle = false)
          *
          * @return \Concrete\Core\Block\Block
          */
    -    public function addBlock($bt, $a, $data)
    +    public function addBlock($bt, $a, $data, ?string $saveMode = SaveMode::SAVE_MODE_REQUEST)
         {
             $app = Application::getFacadeApplication();
             /** @var Connection $db */
             $db = $app->make(Connection::class);
     
             // first we add the block to the system
    -        $nb = $bt->add($data, $this, $a);
    +        $nb = $bt->add($data, $this, $a, $saveMode);
     
             // now that we have a block, we add it to the collectionversions table
     
    
  • concrete/src/Page/Page.php+5 2 modified
    @@ -5,6 +5,7 @@
     use Concrete\Core\Area\Area;
     use Block;
     use CacheLocal;
    +use Concrete\Core\Block\Controller\SaveMode;
     use Concrete\Core\Entity\Page\Summary\CustomPageTemplateCollection;
     use Concrete\Core\Localization\Service\Date;
     use Concrete\Core\Page\Collection\Collection;
    @@ -1192,6 +1193,8 @@ public function updateCollectionAliasExternal($cName, $cLink, $newWindow = 0)
                 } else {
                     $newWindow = 0;
                 }
    +            $cLink = app('helper/security')->sanitizeURL($cLink);
    +            $cName = app('helper/text')->sanitize($cName);
                 $db->executeQuery('update CollectionVersions set cvName = ? where cID = ?', [$cName, $this->cID]);
                 $db->executeQuery('update Pages set cPointerExternalLink = ?, cPointerExternalLinkNewWindow = ? where cID = ?', [$cLink, $newWindow, $this->cID]);
             }
    @@ -2851,12 +2854,12 @@ public function updateGroupsSubCollection($cParentIDString)
          *
          * @return \Concrete\Core\Block\Block
          */
    -    public function addBlock($bt, $a, $data)
    +    public function addBlock($bt, $a, $data, ?string $saveMode = SaveMode::SAVE_MODE_REQUEST)
         {
             if (is_string($a) && $a !== '') {
                 $a = Area::getOrCreate($this, $a);
             }
    -        $b = parent::addBlock($bt, $a, $data);
    +        $b = parent::addBlock($bt, $a, $data, $saveMode);
             $btHandle = $bt->getBlockTypeHandle();
             if ($b->getBlockTypeHandle() == BLOCK_HANDLE_PAGE_TYPE_OUTPUT_PROXY) {
                 $bi = $b->getInstance();
    
  • concrete/src/Summary/Category/Driver/AbstractDriver.php+5 0 modified
    @@ -4,6 +4,7 @@
     use Concrete\Core\Application\Application;
     use Concrete\Core\Application\ApplicationAwareInterface;
     use Concrete\Core\Application\ApplicationAwareTrait;
    +use Concrete\Core\Summary\Category\CategoryMemberInterface;
     use Doctrine\ORM\EntityManager;
     
     defined('C5_EXECUTE') or die("Access Denied.");
    @@ -23,4 +24,8 @@ public function __construct(EntityManager $entityManager)
             $this->entityManager = $entityManager;
         }
     
    +    public function canViewRenderedSummaryTemplates(CategoryMemberInterface $object): bool
    +    {
    +        return false;
    +    }
     }
    
  • concrete/src/Summary/Category/Driver/CalendarEventDriver.php+11 0 modified
    @@ -4,7 +4,9 @@
     
     use Concrete\Core\Calendar\Event\EventOccurrenceService;
     use Concrete\Core\Calendar\Event\EventService;
    +use Concrete\Core\Entity\Calendar\CalendarEventVersionOccurrence;
     use Concrete\Core\Entity\Calendar\Summary\CalendarEventTemplate;
    +use Concrete\Core\Permission\Checker;
     use Concrete\Core\Summary\Category\CategoryMemberInterface;
     use Concrete\Core\Summary\Template\RenderableTemplateInterface;
     
    @@ -23,5 +25,14 @@ public function getMemberSummaryTemplate($templateID): ?RenderableTemplateInterf
             return $this->entityManager->find(CalendarEventTemplate::class, $templateID);
         }
     
    +    /**
    +     * @param CalendarEventVersionOccurrence $object
    +     */
    +    public function canViewRenderedSummaryTemplates(CategoryMemberInterface $object): bool
    +    {
    +        $calendar = $object->getEvent()->getCalendar();
    +        $checker = new Checker($calendar);
    +        return $checker->canEditCalendar();
    +    }
     
     }
    
  • concrete/src/Summary/Category/Driver/DriverInterface.php+1 0 modified
    @@ -14,4 +14,5 @@ public function getCategoryMemberFromIdentifier($identifier): ?CategoryMemberInt
     
         public function getMemberSummaryTemplate($templateID): ?RenderableTemplateInterface;
     
    +    public function canViewRenderedSummaryTemplates(CategoryMemberInterface $object): bool;
     }
    
  • concrete/src/Summary/Category/Driver/PageDriver.php+11 0 modified
    @@ -3,6 +3,7 @@
     
     use Concrete\Core\Entity\Page\Summary\PageTemplate;
     use Concrete\Core\Page\Page;
    +use Concrete\Core\Permission\Checker;
     use Concrete\Core\Summary\Category\CategoryMemberInterface;
     use Concrete\Core\Summary\Template\RenderableTemplateInterface;
     
    @@ -21,4 +22,14 @@ public function getMemberSummaryTemplate($templateID): ?RenderableTemplateInterf
             return $this->entityManager->find(PageTemplate::class, $templateID);
         }
     
    +    /**
    +     * @param Page $object
    +     * @return bool
    +     */
    +    public function canViewRenderedSummaryTemplates(CategoryMemberInterface $object): bool
    +    {
    +        $checker = new Checker($object);
    +        return $checker->canEditPageTemplate();
    +    }
    +
     }
    
  • concrete/src/Url/Validation/InvalidRemoteUrlException.php+9 0 added
    @@ -0,0 +1,9 @@
    +<?php
    +
    +namespace Concrete\Core\Url\Validation;
    +
    +use RuntimeException;
    +
    +class InvalidRemoteUrlException extends RuntimeException
    +{
    +}
    
  • concrete/src/Url/Validation/RemoteUrlRequestOptionsBuilder.php+23 0 added
    @@ -0,0 +1,23 @@
    +<?php
    +
    +namespace Concrete\Core\Url\Validation;
    +
    +use GuzzleHttp\RequestOptions;
    +
    +class RemoteUrlRequestOptionsBuilder
    +{
    +    public function build(ValidatedRemoteUrl $validatedUrl): array
    +    {
    +        $url = $validatedUrl->getUrl();
    +        $host = trim((string) $url->getHost());
    +        $scheme = strtolower((string) $url->getScheme());
    +        $port = $url->getPort();
    +        $port = $port ? $port->get() : null;
    +        $port = $port ? (int) $port : ($scheme === 'http' ? 80 : 443);
    +
    +        return [
    +            RequestOptions::ALLOW_REDIRECTS => false,
    +            'curl' => [CURLOPT_RESOLVE => [sprintf('%s:%d:%s', $host, $port, $validatedUrl->getIp())]],
    +        ];
    +    }
    +}
    
  • concrete/src/Url/Validation/RemoteUrlValidator.php+66 0 added
    @@ -0,0 +1,66 @@
    +<?php
    +
    +namespace Concrete\Core\Url\Validation;
    +
    +use Concrete\Core\Url\Url;
    +use IPLib\Factory as IPFactory;
    +use IPLib\Range\Type as IPRangeType;
    +use RuntimeException;
    +
    +class RemoteUrlValidator
    +{
    +    public function validate(string $url, array $allowedSchemes = ['http', 'https']): ValidatedRemoteUrl
    +    {
    +        try {
    +            $parsedUrl = Url::createFromUrl($url);
    +        } catch (RuntimeException $x) {
    +            throw new InvalidRemoteUrlException($x->getMessage(), 0, $x);
    +        }
    +
    +        $scheme = strtolower((string) $parsedUrl->getScheme());
    +        if (!in_array($scheme, $allowedSchemes, true)) {
    +            throw new InvalidRemoteUrlException('Invalid URL scheme.');
    +        }
    +
    +        $host = trim((string) $parsedUrl->getHost());
    +        if (in_array(strtolower($host), ['', '0', 'localhost'], true)) {
    +            throw new InvalidRemoteUrlException('Invalid URL host.');
    +        }
    +
    +        $ipFormatBlocks = [
    +            '/^\d+$/',
    +            '/^0x[0-9a-f]+$/i',
    +        ];
    +
    +        foreach ($ipFormatBlocks as $block) {
    +            if (preg_match($block, $host) !== 0) {
    +                throw new InvalidRemoteUrlException('Invalid URL host.');
    +            }
    +        }
    +
    +        $ip = null;
    +        if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) !== false) {
    +            $ip = IPFactory::parseAddressString($host);
    +        } elseif (preg_match('/^[0-9.]+$/', $host) !== 0) {
    +            // Reject non-standard numeric host notations like dotted octal.
    +            throw new InvalidRemoteUrlException('Invalid URL host.');
    +        }
    +
    +        if ($ip === null) {
    +            $dnsList = @dns_get_record($host, DNS_A | DNS_AAAA);
    +            while ($ip === null && $dnsList !== false && count($dnsList) > 0) {
    +                $dns = array_shift($dnsList);
    +                $resolvedIp = $dns['ip'] ?? $dns['ipv6'] ?? null;
    +                if ($resolvedIp !== null) {
    +                    $ip = IPFactory::parseAddressString($resolvedIp);
    +                }
    +            }
    +        }
    +
    +        if ($ip === null || $ip->getRangeType() !== IPRangeType::T_PUBLIC) {
    +            throw new InvalidRemoteUrlException('Invalid URL host.');
    +        }
    +
    +        return new ValidatedRemoteUrl($parsedUrl, $ip->toString());
    +    }
    +}
    
  • concrete/src/Url/Validation/ValidatedRemoteUrl.php+57 0 added
    @@ -0,0 +1,57 @@
    +<?php
    +
    +namespace Concrete\Core\Url\Validation;
    +
    +use Concrete\Core\Url\Url;
    +
    +class ValidatedRemoteUrl
    +{
    +    /**
    +     * @var \Concrete\Core\Url\Url
    +     */
    +    protected $url;
    +
    +    /**
    +     * @var string
    +     */
    +    protected $ip;
    +
    +    public function __construct(Url $url, string $ip)
    +    {
    +        $this->url = $url;
    +        $this->ip = $ip;
    +    }
    +
    +    public function getUrl(): Url
    +    {
    +        return $this->url;
    +    }
    +
    +    public function getScheme(): string
    +    {
    +        return strtolower((string) $this->url->getScheme());
    +    }
    +
    +    public function getHost(): string
    +    {
    +        return trim((string) $this->url->getHost());
    +    }
    +
    +    public function getPort(): int
    +    {
    +        $port = $this->url->getPort();
    +        $port = $port ? $port->get() : null;
    +
    +        return $port ? (int) $port : ($this->getScheme() === 'http' ? 80 : 443);
    +    }
    +
    +    public function getIp(): string
    +    {
    +        return $this->ip;
    +    }
    +
    +    public function __toString(): string
    +    {
    +        return (string) $this->url;
    +    }
    +}
    
  • concrete/src/User/UserInfo.php+6 2 modified
    @@ -494,6 +494,9 @@ public function getUserObject()
          */
         public function update($data)
         {
    +        $data = is_array($data) ? $data : [];
    +        $allowPasswordUpdate = ($data['_allowPasswordUpdate'] ?? null) === true;
    +        $allowIgnoredIPMismatchesUpdate = ($data['_allowIgnoredIPMismatchesUpdate'] ?? null) === true;
             $uID = (int)$this->getUserID();
             if ($uID === 0) {
                 $result = false;
    @@ -533,7 +536,7 @@ public function update($data)
                         $values[] = $data['uHomeFileManagerFolderID'];
                     }
                 }
    -            if (isset($data['uPassword']) && (string)$data['uPassword'] !== '') {
    +            if ($allowPasswordUpdate && isset($data['uPassword']) && (string)$data['uPassword'] !== '') {
                     if (isset($data['uPasswordConfirm']) && $data['uPassword'] === $data['uPasswordConfirm']) {
                         $passwordChangedOn = $this->application->make('date')->getOverridableNow();
                         $fields[] = 'uPassword = ?';
    @@ -548,7 +551,7 @@ public function update($data)
                         $result = null;
                     }
                 }
    -            if (is_array($data['ignoredIPMismatches'] ?? null)) {
    +            if ($allowIgnoredIPMismatchesUpdate && is_array($data['ignoredIPMismatches'] ?? null)) {
                     $fields[] = 'ignoredIPMismatches = ?';
                     $values[] = (new SimpleArrayType())->convertToDatabaseValue($data['ignoredIPMismatches'], $this->connection->getDatabasePlatform());
                 }
    @@ -712,6 +715,7 @@ public function markValidated()
         public function changePassword($newPassword)
         {
             return $this->update([
    +            '_allowPasswordUpdate' => true,
                 'uPassword' => $newPassword,
                 'uPasswordConfirm' => $newPassword,
                 'uIsPasswordReset' => false,
    
  • concrete/src/Utility/Service/Url.php+9 8 modified
    @@ -28,15 +28,16 @@ public function setVariable($variable, $value = false, $url = false)
                 // sanitizeString() removes tags like <script>… and similar markup.
                 $url = Loader::helper('security')->sanitizeString($_SERVER['REQUEST_URI']);
                 $url = $encodeQuotesAndStripCRLF($url);
    -        } elseif (strpos($url, '?') === false) {
    -            // Base URL provided without a query: protect it too (in case it contains quotes).
    +        } else {
    +            // Base URL provided by the caller: protect it too (in case it contains quotes).
                 $url = $encodeQuotesAndStripCRLF($url);
    -
    -            // Append the current query string, after sanitizing and applying the same light encoding.
    -            $qs = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
    -            $qs = Loader::helper('security')->sanitizeString($qs);
    -            if ($qs !== false && $qs !== '') {
    -                $url .= '?' . $encodeQuotesAndStripCRLF($qs);
    +            if (strpos($url, '?') === false) {
    +                // Append the current query string, after sanitizing and applying the same light encoding.
    +                $qs = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
    +                $qs = Loader::helper('security')->sanitizeString($qs);
    +                if ($qs !== false && $qs !== '') {
    +                    $url .= '?' . $encodeQuotesAndStripCRLF($qs);
    +                }
                 }
             }
     
    
  • concrete/themes/atomik/account.php+1 1 modified
    @@ -8,7 +8,7 @@
         // spacing. If we add an element between the container and the outer page class it breaks this. ?>
         <div class="row">
             <div class="col-md-12">
    -            <h1><?=$c->getCollectionName()?></h1>
    +            <h1><?=h($c->getCollectionName())?></h1>
     
                 <?php
                 View::element(
    
  • concrete/themes/dashboard/main.js+2 2 modified
  • concrete/views/dialogs/file/usage.php+0 58 removed
    @@ -1,58 +0,0 @@
    -<?php
    -
    -use Concrete\Core\Entity\Statistics\UsageTracker\FileUsageRecord;
    -use Concrete\Core\Page\Page;
    -use Concrete\Core\Support\Facade\Url;
    -
    -/** @var FileUsageRecord[] $records */
    -
    -?>
    -<div class="ccm-ui">
    -    <table class="table table-striped">
    -        <thead>
    -            <tr>
    -                <td>
    -                    <?php echo  t('Page ID') ?>
    -                </td>
    -
    -                <td>
    -                    <?php echo  t('Version') ?>
    -                </td>
    -
    -                <td>
    -                    <?php echo  t('Handle') ?>
    -                </td>
    -
    -                <td>
    -                    <?php echo  t('Location') ?>
    -                </td>
    -            </tr>
    -        </thead>
    -
    -        <tbody>
    -            <?php foreach ($records as $record): ?>
    -                <?php $page = Page::getByID($record->getCollectionID()); ?>
    -
    -                <tr>
    -                    <td>
    -                        <?php echo  $page->getCollectionID() ?>
    -                    </td>
    -
    -                    <td>
    -                        <?php echo  $page->getVersionID() ?>
    -                    </td>
    -
    -                    <td>
    -                        <?php echo  $page->getCollectionHandle() ?>
    -                    </td>
    -
    -                    <td>
    -                        <a target="_blank" href="<?php echo Url::to($page) ?>">
    -                            <?php echo h($page->getCollectionPath() ?: '/') ?>
    -                        </a>
    -                    </td>
    -                </tr>
    -            <?php endforeach; ?>
    -        </tbody>
    -    </table>
    -</div>
    
  • concrete/views/dialogs/file/versions.php+2 0 modified
    @@ -102,6 +102,7 @@
                 'fileVersions' => $versions,
                 'canPreviewFileVersion' => $canPreviewFileVersion,
                 'canDeleteFileVersion' => $canDeleteFileVersion,
    +            'approveToken' => $token->generate('approve_file_version'),
             ]) ?>;
         },
         watch: {
    @@ -115,6 +116,7 @@
                     data: {
                         fID: this.fileID,
                         fvID: this.activeFileVersionID,
    +                    <?= json_encode($token::DEFAULT_TOKEN_NAME) ?>: this.approveToken,
                     },
                     success: () => {
                         this.busy = false;
    
  • concrete/views/oauth/authorize.php+1 1 modified
    @@ -50,7 +50,7 @@
                 <?php
                 if (!$authorize) {
                     ?>
    -                <h3 class="text-center"><?= t('Sign in to %s', "<strong>{$client->getName()}</strong>") ?></h3>
    +                <h3 class="text-center"><?= t('Sign in to %s', "<strong>{h($client->getName())}</strong>") ?></h3>
                     <?php
                 }
                 ?>
    
  • tests/tests/Controller/Backend/FileTest.php+3 4 modified
    @@ -67,14 +67,13 @@ public static function remoteUrlsToTry(): iterable
             $octal3 = '01002004010';
             $integer = '134744072';
     
    -        // Test hexadecimal IPs get caught as local
    +        // Standard public IPv4 hosts remain valid, but non-standard numeric notations are rejected.
             yield ['http://' . $simpleIp]; // This is allowed because it's an external IP
             yield ['http://' . $hex, UserMessageException::class, '/The URL &quot;.+?&quot; is not valid./'];
    -        yield ['http://' . $octal]; // This form is allowed because it at least converts properly in ip-lib
    -        yield ['http://' . $octal2]; // Same as the first octal
    +        yield ['http://' . $octal, UserMessageException::class, '/The URL &quot;.+?&quot; is not valid./'];
    +        yield ['http://' . $octal2, UserMessageException::class, '/The URL &quot;.+?&quot; is not valid./'];
             yield ['http://' . $octal3, UserMessageException::class, '/The URL &quot;.+?&quot; is not valid./'];
             yield ['http://' . $integer, UserMessageException::class, '/The URL &quot;.+?&quot; is not valid./'];
         }
     
     }
    -
    
  • tests/tests/Url/Validation/RemoteUrlValidationTest.php+49 0 added
    @@ -0,0 +1,49 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Concrete\Tests\Url\Validation;
    +
    +use Concrete\Core\Url\Url;
    +use Concrete\Core\Url\Validation\InvalidRemoteUrlException;
    +use Concrete\Core\Url\Validation\RemoteUrlRequestOptionsBuilder;
    +use Concrete\Core\Url\Validation\RemoteUrlValidator;
    +use Concrete\Core\Url\Validation\ValidatedRemoteUrl;
    +use Concrete\Tests\TestCase;
    +use GuzzleHttp\RequestOptions;
    +
    +class RemoteUrlValidationTest extends TestCase
    +{
    +    public function testValidateWrapsParsedUrlAndResolvedIp(): void
    +    {
    +        $validatedUrl = (new RemoteUrlValidator())->validate('http://8.8.8.8:8080/feed');
    +
    +        $this->assertInstanceOf(ValidatedRemoteUrl::class, $validatedUrl);
    +        $this->assertInstanceOf(Url::class, $validatedUrl->getUrl());
    +        $this->assertSame('http', $validatedUrl->getScheme());
    +        $this->assertSame('8.8.8.8', $validatedUrl->getHost());
    +        $this->assertSame(8080, $validatedUrl->getPort());
    +        $this->assertSame('8.8.8.8', $validatedUrl->getIp());
    +        $this->assertSame('http://8.8.8.8:8080/feed', (string) $validatedUrl);
    +    }
    +
    +    public function testBuildUsesWrappedUrlComponentsForCurlResolve(): void
    +    {
    +        $validatedUrl = (new RemoteUrlValidator())->validate('https://8.8.8.8/feed');
    +        $options = (new RemoteUrlRequestOptionsBuilder())->build($validatedUrl);
    +
    +        $this->assertFalse($options[RequestOptions::ALLOW_REDIRECTS]);
    +        $this->assertSame(
    +            ['8.8.8.8:443:8.8.8.8'],
    +            $options['curl'][CURLOPT_RESOLVE]
    +        );
    +    }
    +
    +    public function testValidateRejectsLocalhost(): void
    +    {
    +        $this->expectException(InvalidRemoteUrlException::class);
    +        $this->expectExceptionMessage('Invalid URL host.');
    +
    +        (new RemoteUrlValidator())->validate('http://localhost/feed');
    +    }
    +}
    
c3a7f6374907

Update permissionkeys table before removing duplicate category

https://github.com/concretecms/concretecmshissyMay 17, 2026Fixed in 9.5.1via llm-release-walk
1 file changed · +1 0
  • concrete/src/Updater/Migrations/Migrations/Version20210729191135.php+1 0 modified
    @@ -19,6 +19,7 @@ public function upgradeDatabase()
             $marketplaceExists = $db->fetchOne("select count(*) from PermissionKeyCategories where pkCategoryHandle = 'marketplace'");
             $marketplaceNewsflowExists = $db->fetchOne("select count(*) from PermissionKeyCategories where pkCategoryHandle = 'marketplace_newsflow'");
             if ($marketplaceExists && $marketplaceNewsflowExists) {
    +            $db->executeStatement("update PermissionKeys set pkCategoryID = (select pkCategoryID from PermissionKeyCategories where pkCategoryHandle = 'marketplace') where pkCategoryID = (select pkCategoryID from PermissionKeyCategories where pkCategoryHandle = 'marketplace_newsflow')");
                 $db->executeStatement("delete from PermissionKeyCategories where pkCategoryHandle = 'marketplace_newsflow'");
             } elseif ($marketplaceNewsflowExists) {
                 $db->executeStatement("update PermissionKeyCategories set pkCategoryHandle = 'marketplace' where pkCategoryHandle = 'marketplace_newsflow'");
    

Vulnerability mechanics

Root cause

"The `/ccm/frontend/conversations/message_page` endpoint lacks authorization checks, allowing any unauthenticated user to retrieve the full content of any conversation message."

Attack vector

An unauthenticated attacker sends HTTP GET requests to the `/ccm/frontend/conversations/message_page` endpoint, passing a conversation message ID as a parameter. The endpoint returns the full message content and associated file attachment download URLs without verifying the user's identity or permissions. This allows enumeration of all conversation messages, including those from restricted pages, member-only areas, and the moderation queue. The advisory notes that Concrete CMS 9.5.0 and below are affected, and the CVSS vector indicates network-based exploitation with no privileges required.

Affected code

The advisory identifies the endpoint `/ccm/frontend/conversations/message_page` as the vulnerable code path. No patch file in the bundle modifies this endpoint directly. The bundle includes patches to `concrete/controllers/backend/file.php`, `concrete/src/Api/OAuth/Controller.php`, `concrete/controllers/dialog/express/association/reorder.php`, `concrete/src/Url/Validation/RemoteUrlValidator.php`, and `concrete/controllers/single_page/dashboard/extend/update.php` [patch_id=1291277], but none of these address the conversation messages endpoint.

What the fix does

The patch bundle does not contain a direct fix for the conversation messages endpoint. Instead, the included patches add CSRF token validation to several file controller actions [patch_id=1291277], refactor remote URL import validation into a dedicated `RemoteUrlValidator` class [patch_id=1291277], fix an access control check in the Express association reorder controller from `canViewExpressEntry` to `canEditExpressEntry` [patch_id=1291277], add CSRF and POST-method checks to the package update controller [patch_id=1291277], and improve OAuth login error handling [patch_id=1291277]. These changes address related security issues but the specific IDOR vulnerability in the conversations endpoint is not shown in the provided diff.

Preconditions

  • authNo authentication required; the endpoint is accessible to unauthenticated users.
  • networkNetwork access to a Concrete CMS instance running version 9.5.0 or below.
  • inputAttacker must know or enumerate valid conversation message IDs.

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

References

1

News mentions

0

No linked articles in our index yet.