Skip to content

Commit 056971c

Browse files
committed
Harden reorder, save, and update against crafted payloads
- Reject duplicate item IDs in reorder tree payload (prevents cycles) - Add visited-set guard in menus_item_can_be_enabled() parent walk - On edit, derive category from DB not POST (prevents cross-category moves) - Protected items cannot be re-parented via tampered form - Wrap update helpers in try/catch so failures propagate to module errors - Include items_protected in seed item lookup (prevents overwriting custom items)
1 parent a4ef68d commit 056971c

2 files changed

Lines changed: 49 additions & 11 deletions

File tree

htdocs/modules/system/admin/menus/main.php

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,14 @@ function menus_item_can_be_enabled(XoopsMenusItemsHandler $itemHandler, XoopsMen
159159
return false;
160160
}
161161

162-
// Walk the full parent chain
162+
// Walk the full parent chain (with visited-set guard against cycles)
163163
$parentId = (int) $item->getVar('items_pid');
164+
$visited = [];
164165
while ($parentId > 0) {
166+
if (isset($visited[$parentId])) {
167+
break; // Cycle detected — stop walking
168+
}
169+
$visited[$parentId] = true;
165170
$parent = $itemHandler->get($parentId);
166171
if (!is_object($parent)) {
167172
break;
@@ -481,20 +486,25 @@ function menus_assign_template_defaults(\XoopsTpl $tpl, string $op): void
481486
menus_require_token(false);
482487
$itemHandler = menus_item_handler();
483488
$itemId = Request::getInt('items_id', 0, 'POST');
484-
$catId = Request::getInt('items_cid', 0, 'POST');
485-
$xoopsTpl->assign('category_id', $catId);
486489

487490
if ($itemId > 0) {
488491
$item = $itemHandler->get($itemId);
489492
if (!is_object($item) || $item->isNew()) {
490493
redirect_header(MENUS_ADMIN_URL, 3, _AM_SYSTEM_MENUS_ERROR_ITEMNOTFOUND);
491494
}
495+
// Existing item: derive category from DB, not POST (prevents cross-category moves via tampered form)
496+
$catId = (int) $item->getVar('items_cid');
497+
$parentId = (int) $item->getVar('items_protected')
498+
? (int) $item->getVar('items_pid') // Protected items keep their parent
499+
: Request::getInt('items_pid', 0, 'POST');
492500
} else {
493-
$item = $itemHandler->create();
501+
$item = $itemHandler->create();
502+
$catId = Request::getInt('items_cid', 0, 'POST');
503+
$parentId = Request::getInt('items_pid', 0, 'POST');
494504
}
505+
$xoopsTpl->assign('category_id', $catId);
495506

496507
$isProtected = (bool) $item->getVar('items_protected');
497-
$parentId = Request::getInt('items_pid', 0, 'POST');
498508

499509
// Validate parent
500510
if ($parentId > 0) {
@@ -675,6 +685,28 @@ function menus_assign_template_defaults(\XoopsTpl $tpl, string $op): void
675685
menus_send_json(false, ['message' => _AM_SYSTEM_MENUS_ERROR_ITEMDEPTH]);
676686
}
677687

688+
// Collect all IDs from the tree and reject duplicates
689+
$seenIds = [];
690+
$collectIds = function (array $nodes) use (&$collectIds, &$seenIds): bool {
691+
foreach ($nodes as $node) {
692+
$id = (int) ($node['id'] ?? 0);
693+
if ($id <= 0) {
694+
continue;
695+
}
696+
if (isset($seenIds[$id])) {
697+
return false; // Duplicate — reject entire payload
698+
}
699+
$seenIds[$id] = true;
700+
if (!empty($node['children']) && !$collectIds($node['children'])) {
701+
return false;
702+
}
703+
}
704+
return true;
705+
};
706+
if (!$collectIds($tree)) {
707+
menus_send_json(false, ['message' => 'Duplicate item IDs in tree']);
708+
}
709+
678710
// Walk the tree and update positions
679711
$itemHandler = menus_item_handler();
680712
$position = 0;

htdocs/modules/system/include/update.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,14 @@ function system_menu_update(XoopsModule $module): void
149149
$db = \XoopsDatabaseFactory::getDatabaseConnection();
150150
$mid = (int) $module->getVar('mid');
151151

152-
system_menu_create_tables($db);
153-
system_menu_normalize_schema($db);
154-
system_menu_migrate_unsafe_urls($db);
155-
system_menu_seed_defaults($db, $mid);
152+
try {
153+
system_menu_create_tables($db);
154+
system_menu_normalize_schema($db);
155+
system_menu_migrate_unsafe_urls($db);
156+
system_menu_seed_defaults($db, $mid);
157+
} catch (\Throwable $e) {
158+
$module->setErrors('Menu migration failed: ' . $e->getMessage());
159+
}
156160
}
157161

158162
/**
@@ -336,11 +340,13 @@ function system_menu_ensure_item(XoopsMySQLDatabase $db, int $categoryId, array
336340
{
337341
$table = $db->prefix('menusitems');
338342
$result = $db->query(sprintf(
339-
"SELECT `items_id`, `items_active` FROM `%s` WHERE `items_cid` = %d AND `items_title` = %s"
343+
"SELECT `items_id`, `items_active` FROM `%s`"
344+
. " WHERE `items_cid` = %d AND `items_title` = %s AND `items_protected` = %d"
340345
. " ORDER BY `items_id` ASC",
341346
$table,
342347
$categoryId,
343-
$db->quote($definition['title'])
348+
$db->quote($definition['title']),
349+
(int) $definition['protected']
344350
));
345351
if ($db->isResultSet($result) && ($result instanceof \mysqli_result) && ($row = $db->fetchArray($result))) {
346352
$active = (int) ($row['items_active'] ?? $definition['active']);

0 commit comments

Comments
 (0)