Root cause
Authentication & access control introduced
new: [event-template(builder)] Tag taxonomy + galaxy type pickers
How AI contributed: Direct introductionPublished Jun 12, 2026 in
misp/mispAll 2 findings in this repository
The AI-written galaxy picker in EventTemplatesController::__setBuilderConfig() loaded galaxy types with an enabled-flag condition and nothing else, so the event template builder listed galaxies belonging to other organisations, including org-only entries with zero distribution. Authenticated users could therefore see and select restricted galaxy data in the template editor. The fix routes the query through $this->Galaxy->buildConditions($this->Auth->user()), after an intermediate condition whose Galaxy.distribution comparison always evaluated true.
The galaxy query in __setBuilderConfig() filtered on the enabled flag alone, leaving nothing to tie the returned types to the requesting user or organisation. The later fix delegates visibility to $this->Galaxy->buildConditions($this->Auth->user()).
Root cause
new: [event-template(builder)] Tag taxonomy + galaxy type pickers
Fix
fix: [security] exposing non visible galaxies for template creation
Case facts
@@ -606,6 +606,29 @@ if (el.type === 'object_reference') { refreshObjectFieldSelects($pane, el); }+ if (el.type === 'tag_field' || el.type === 'galaxy_field') {+ refreshMultipickerSummary($pane, el);+ }+ }++ function refreshMultipickerSummary($pane, el) {+ $pane.querySelectorAll('[data-et-multipicker-summary-for]').forEach(function ($s) {+ var field = $s.getAttribute('data-et-multipicker-summary-for');+ var values = getDeep(el, field);+ if (!Array.isArray(values) || values.length === 0) {+ $s.innerHTML = '<em>' ++ (field === 'restrict_taxonomies' ? '(any taxonomy)' : '(any galaxy type)') ++ '</em>';+ return;+ }+ $s.innerHTML = '';+ values.forEach(function (v) {+ var $chip = document.createElement('span');+ $chip.className = 'et-chip';+ $chip.textContent = v;+ $s.appendChild($chip);+ });+ }); } function refreshAttributeTypeOptions($pane, el) {@@ -771,6 +794,132 @@ renderProperties(); } + // ---------------------------------------------------------------+ // Generic multi-picker modal — used for tag taxonomies + galaxy types.+ // ---------------------------------------------------------------++ // Tracks which element/field the modal is currently editing.+ var multipickerCtx = null;++ function wireMultipicker() {+ document.addEventListener('click', function (e) {+ var $btn = e.target.closest('[data-et-open-multipicker]');+ if ($btn) {+ e.preventDefault();+ openMultipicker(+ $btn.getAttribute('data-et-open-multipicker'),+ $btn.getAttribute('data-et-multipicker-field'),+ $btn.getAttribute('data-et-multipicker-title') || 'Select items'+ );+ }+ });++ var $search = document.getElementById('et-multipicker-search');+ if ($search) {+ $search.addEventListener('input', function () {+ filterMultipickerList($search.value);+ });+ }++ var $apply = document.getElementById('et-multipicker-apply');+ if ($apply) {+ $apply.addEventListener('click', function (e) {+ e.preventDefault();+ applyMultipicker();+ });+ }+ }++ function openMultipicker(source, field, title) {+ if (!state.selectedId) { return; }+ var el = findElement(state.selectedId);+ if (!el) { return; }+ var items = (cfg.multipickerSources && cfg.multipickerSources[source]) || [];+ var current = getDeep(el, field);+ var selectedSet = {};+ if (Array.isArray(current)) {+ current.forEach(function (v) { selectedSet[v] = true; });+ }+ multipickerCtx = {source: source, field: field, elementId: el.id};++ var $title = document.getElementById('et-multipicker-title');+ if ($title) { $title.textContent = title; }++ var $list = document.getElementById('et-multipicker-list');+ if ($list) {+ $list.innerHTML = '';+ if (!items.length) {+ $list.innerHTML = '<div style="padding:20px; text-align:center; color:#888;">' +@@ -660,6 +660,55 @@ private function __setBuilderConfig() ); } $this->set('objectTemplatesAvailable', $objectTemplates);++ $this->loadModel('Taxonomy');+ $taxonomyRows = $this->Taxonomy->find('all', array(+ 'recursive' => -1,+ 'conditions' => array('Taxonomy.enabled' => true),+ 'fields' => array(+ 'Taxonomy.namespace',+ 'Taxonomy.description',+ 'Taxonomy.version',+ ),+ 'order' => array('Taxonomy.namespace' => 'ASC'),+ ));+ $taxonomies = array();+ foreach ($taxonomyRows as $r) {+ $t = $r['Taxonomy'];+ $taxonomies[] = array(+ 'value' => (string)$t['namespace'],+ 'label' => (string)$t['namespace'],+ 'description' => (string)($t['description'] ?? ''),+ );+ }+ $this->set('taxonomiesAvailable', $taxonomies);++ $this->loadModel('Galaxy');+ $galaxyRows = $this->Galaxy->find('all', array(+ 'recursive' => -1,+ 'conditions' => array('Galaxy.enabled' => true),+ 'fields' => array(+ 'Galaxy.type',+ 'Galaxy.description',+ ),+ 'group' => array('Galaxy.type'),+ 'order' => array('Galaxy.type' => 'ASC'),+ ));+ $galaxyTypes = array();+ $seenTypes = array();+ foreach ($galaxyRows as $r) {+ $t = $r['Galaxy']['type'];+ if ($t === null || $t === '' || isset($seenTypes[$t])) {+ continue;+ }+ $seenTypes[$t] = true;+ $galaxyTypes[] = array(+ 'value' => (string)$t,+ 'label' => (string)$t,+ 'description' => (string)($r['Galaxy']['description'] ?? ''),+ );+ }+ $this->set('galaxyTypesAvailable', $galaxyTypes); } private function __renderDependencyMissing(array $missing)@@ -32,6 +32,10 @@ $objectTemplates = isset($objectTemplatesAvailable) ? $objectTemplatesAvailable : [];+ $multipickerSources = [+ 'taxonomies' => isset($taxonomiesAvailable) ? $taxonomiesAvailable : [],+ 'galaxyTypes' => isset($galaxyTypesAvailable) ? $galaxyTypesAvailable : [],+ ]; $paletteButtons = [ 'section' => ['label' => __('Section'), 'icon' => 'folder'],@@ -153,6 +157,29 @@ #et-ot-picker-modal { width: 640px; max-height: 80vh; } #et-ot-picker-modal .modal-body { max-height: calc(80vh - 120px); overflow: hidden; display: flex; flex-direction: column; gap: 8px; } #et-ot-picker-list { flex: 1; overflow-y: auto; border: 1px solid #eee; border-radius: 3px; }+#et-multipicker-modal { width: 560px; }+#et-multipicker-modal .modal-body { display: flex; flex-direction: column; gap: 8px; }+#et-multipicker-modal label.et-multipicker-item {+ display: block;+ padding: 6px 10px;+ border-bottom: 1px solid #f3f3f3;+ margin: 0;+ cursor: pointer;+}+#et-multipicker-modal label.et-multipicker-item:hover { background: #f0f8ff; }+#et-multipicker-modal label.et-multipicker-item input[type=checkbox] { margin-right: 8px; }+#et-multipicker-modal .et-multipicker-label { font-weight: 500; }+#et-multipicker-modal .et-multipicker-desc { display: block; color: #888; font-size: 11px; margin-left: 24px; }+.eventTemplates.builder .et-multipicker-summary { font-size: 12px; color: #333; }+.eventTemplates.builder .et-multipicker-summary .et-chip {+ display: inline-block;+ background: #eef4fa;+ border: 1px solid #cde1f2;+ border-radius: 3px;+ padding: 1px 6px;+ margin: 0 2px 2px 0;+ font-size: 11px;+} </style> <div class="eventTemplates builder form"> <h2><?php echo h($pageTitle); ?></h2>@@ -279,6 +306,28 @@ </div> </div> +<div id="et-multipicker-modal" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">+ <div class="modal-header">+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>+ <h3 id="et-multipicker-title"><?php echo __('Select items'); ?></h3>+ </div>+ <div class="modal-body">+ <input type="text" id="et-multipicker-search" class="input-block-level"+ placeholder="<?php echo __('Filter…'); ?>">+ <div id="et-multipicker-list" style="max-height:420px; overflow-y:auto; border:1px solid #eee; border-radius:3px;">+ <!-- populated by JS on open -->+ </div>+ </div>+ <div class="modal-footer">+ <button type="button" class="btn" data-dismiss="modal">+ <?php echo __('Cancel'); ?>+ </button>+ <button type="button" class="btn btn-primary" id="et-multipicker-apply">+ <?php echo __('Apply'); ?>+ </button>+ </div>+</div>+ <script> window.ET_BUILDER_CONFIG = { mode: <?php echo json_encode($builderMode); ?>,@@ -287,7 +336,8 @@ envelope: <?php echo json_encode($envelope); ?>, definition: <?php echo json_encode($initialDefinition); ?>, attrCategories: <?php echo json_encode($attrCategories); ?>,- objectTemplates: <?php echo json_encode($objectTemplates); ?>+ objectTemplates: <?php echo json_encode($objectTemplates); ?>,+ multipickerSources: <?php echo json_encode($multipickerSources); ?> }; </script> <?php@@ -1010,21 +1010,16 @@ private function __setBuilderConfig() ); } $this->set('taxonomiesAvailable', $taxonomies);- $orgCondition = $this->_isSiteAdmin()- ? array()- : array(- 'OR' => array(- 'Galaxy.org_id' => (int)$this->Auth->user('org_id'),- 'Galaxy.distribution' > 0,- ),- );- $this->loadModel('Galaxy'); $galaxyRows = $this->Galaxy->find('all', array( 'recursive' => -1, 'conditions' => array( 'Galaxy.enabled' => true,- $orgCondition,+ // Use the canonical galaxy visibility filter. The previous hand-rolled condition+ // `'Galaxy.distribution' > 0` was a PHP comparison (always evaluating to true), not a+ // query condition, so every org's galaxies - including org-only (distribution 0) custom+ // galaxies belonging to other orgs - leaked into the template builder list.+ $this->Galaxy->buildConditions($this->Auth->user()), ), 'fields' => array( 'Galaxy.type',AI-assisted change fc02ff10f0052d9b188240b7757fd63f137d63f70261d2d777ea0f4d73d1b567 · Fix b2075a351f2055414840b4f816c30a1225a5d4a9635440b82e672a5725a36723