Skip to content

Fix security audit findings: XSS, eval, insecure reset, null guards#1636

Merged
mambax7 merged 8 commits intoXOOPS:masterfrom
mambax7:fix/security-audit-remediation
Mar 8, 2026
Merged

Fix security audit findings: XSS, eval, insecure reset, null guards#1636
mambax7 merged 8 commits intoXOOPS:masterfrom
mambax7:fix/security-audit-remediation

Conversation

@mambax7
Copy link
Copy Markdown
Collaborator

@mambax7 mambax7 commented Mar 8, 2026

Summary

Fixes all 13 remaining issues identified in the security/critical audit and migration audit:

  • Account takeover: Disable insecure profile lostpass flow (deterministic 5-char md5 token, no expiry, no rate limit). All resets now go through the secure core flow with XoopsTokenHandler (random, one-time, expiring tokens)
  • XSS: Escape $_SERVER['PHP_SELF'] in xoopscomments.php (use SCRIPT_NAME instead) and REQUEST_URI in site-closed template
  • eval() removal: Remove eval() path in file cache engine (default serialize to true); gate PHP block eval() behind XOOPS_ALLOW_PHP_BLOCKS constant (default false)
  • Object injection: Add allowed_classes => false to all 5 bare unserialize() calls across object.php, configitem.php, cache/model.php, profile/update.php
  • Null guards: Add missing null checks after getUser()/get() in system admin users (delete, bulk delete, activate) and findusers.php
  • CSRF hardening: Pin ok delete confirmation parameter to POST only
  • Content corruption: Use getText() for user_sig, bio, and com_text fields that legitimately contain HTML
  • Password handling: Fix remaining getString() usage for password fields in lostpass.php
  • PHP 8.2 compat: Remove vendor wideimage demo/ directory containing fatal __autoload()
  • Superglobal migration: Migrate protector gtickets.php and stopforumspam filter to Xmf\Request

Files changed (18 modified + 67 deleted)

Category Files
Password reset modules/profile/preloads/core.php, modules/profile/lostpass.php, lostpass.php
XSS fixes class/xoopscomments.php, themes/default/.../system_siteclosed.tpl
eval/unserialize class/cache/file.php, kernel/block.php, modules/system/class/block.php, kernel/object.php, kernel/configitem.php, class/cache/model.php, modules/profile/include/update.php
Null guards & CSRF modules/system/admin/users/main.php, include/findusers.php
Content fields edituser.php, include/comment_post.php, modules/system/admin/users/main.php
Superglobals xoops_lib/modules/protector/class/gtickets.php, xoops_lib/.../postcommon_register_stopforumspam.php
PHP 8.2 compat xoops_lib/vendor/smottt/wideimage/demo/ (deleted)

Test plan

  • Verify password reset flow works via core lostpass.php (email submission, token link, password change)
  • Verify direct access to modules/profile/lostpass.php redirects to core lostpass
  • Verify comment posting preserves HTML content in comment body
  • Verify user signature and bio save/display with special characters
  • Verify admin user delete (single and bulk) with valid and invalid user IDs
  • Verify admin user activation with valid and invalid user IDs
  • Verify cache read/write still works with serialize default changed to true
  • Verify PHP blocks show empty output when XOOPS_ALLOW_PHP_BLOCKS is not defined
  • Verify PHP blocks execute when XOOPS_ALLOW_PHP_BLOCKS is set to true
  • Verify site-closed login form works correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation

    • Added a consolidated remediation record summarizing phased security fixes, statuses, and upgrade notes.
  • New Features

    • Admin user list performance improved via batch group prefetch.
    • Cache now uses serialized storage by default for consistent handling.
  • Bug Fixes

    • Added runtime guards and stronger input validation to prevent invalid actions and skip missing targets.
    • Replaced deprecated local lost-password flow with redirect to core flow.
  • Security

    • Hardened deserialization, removed unsafe evaluation paths, improved URL/output sanitization, and disabled PHP block execution unless explicitly allowed.
  • Chores

    • Removed legacy demo and font demo artifacts.

Critical:
- Disable insecure profile lostpass (md5 5-char token) and redirect to
  secure core flow with XoopsTokenHandler
- Fix password field in lostpass.php to use raw getVar() instead of getString()
- Add null guards after getUser() in system admin users (delete, bulk
  delete, activate) to prevent fatal errors on invalid IDs
- Pin 'ok' delete confirmation to POST to prevent GET-based CSRF

High:
- Fix reflected XSS via raw $_SERVER['PHP_SELF'] in xoopscomments.php
  by using escaped SCRIPT_NAME instead
- Escape REQUEST_URI in site-closed template
- Use getText() for content fields (comment body, user_sig, bio) that
  legitimately contain HTML/special characters
- Add null guard for rank_obj in findusers.php

Security hardening:
- Remove eval() path in file cache engine, default serialize to true
- Gate PHP block eval() behind XOOPS_ALLOW_PHP_BLOCKS constant (default off)
- Add allowed_classes=>false to all unserialize() calls (object.php,
  configitem.php, cache/model.php, profile/update.php)
- Remove @ error suppression from configitem.php unserialize

Compatibility:
- Remove vendor wideimage demo directory (PHP 8.2 fatal: __autoload())

Consistency:
- Migrate protector gtickets.php and stopforumspam filter from raw
  superglobals to Xmf\Request

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 10:45
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a security remediation document; hardens deserialization and input handling; removes eval-based cache legacy reads; sanitizes script/form URLs; restricts PHP block execution at runtime; consolidates lost-password flow to core; prefetches groups to avoid N+1 queries; removes WideImage demo suite and helper classes.

Changes

Cohort / File(s) Summary
Remediation roadmap
docs/remediation_proposal.md
Add Security Audit Remediation Record with phased fixes, files-changed mapping, deferred items and upgrade notes.
Unserialize hardening
htdocs/class/cache/model.php, htdocs/kernel/configitem.php, htdocs/kernel/object.php, htdocs/modules/profile/include/update.php
All unserialize() calls now use ['allowed_classes' => false] and add input checks/error handling to avoid unsafe deserialization.
Cache engine: eval removal
htdocs/class/cache/file.php
Enable serialization by default; legacy non-serialized cache entries are no longer evaluated via eval(), treated as misses to avoid code execution.
PHP block execution guard
htdocs/kernel/block.php, htdocs/modules/system/class/block.php
If block type is PHP and XOOPS_ALLOW_PHP_BLOCKS is not true, log warning and return empty content to prevent runtime PHP block execution.
Request/input centralization
htdocs/xoops_lib/.../protector/class/gtickets.php, htdocs/xoops_lib/.../protector/filters_disabled/postcommon_register_stopforumspam.php, htdocs/include/comment_post.php, htdocs/edituser.php, htdocs/lostpass.php, htdocs/modules/system/admin/users/main.php
Replace direct superglobal access with Xmf\Request helpers or switch to Request::getText/getVar for richer/raw input handling where appropriate.
Lost-password consolidation
htdocs/modules/profile/lostpass.php, htdocs/modules/profile/preloads/core.php, htdocs/lostpass.php
Module-level lostpass logic removed/disabled; profile module redirects to core lostpass; preload hook made no-op; core lostpass reads raw input with Request::getVar.
User admin robustness & optimization
htdocs/modules/system/admin/users/main.php
Validate user objects for delete/activate flows; skip missing users in batch ops; prefetch groups for page users to remove per-user DB queries (N+1 fix).
Find-users rank validation
htdocs/include/findusers.php
Guard rank handling: validate rankId and rank_obj before usage; use Criteria('rank', $rankId) for special ranks; guard post-range criteria.
XSS / URL sanitization
htdocs/class/xoopscomments.php, htdocs/themes/default/modules/system/system_siteclosed.tpl
Replace PHP_SELF/raw REQUEST_URI usage with escaped SCRIPT_NAME or Smarty-escaped redirect field to mitigate reflected XSS in internal URLs.
Protector input centralization
htdocs/xoops_lib/modules/protector/...
Use Xmf\Request to read ticket and registration fields rather than direct $_POST/$_GET.
WideImage demo removal
htdocs/xoops_lib/vendor/smottt/wideimage/demo/*
Remove entire WideImage demo suite: index, screen, image endpoint, demo implementations, helper Field/Request/Demo classes, demo fonts and config files (large deletions).

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • redheadedrod
  • djculex
  • tad0616
  • Philoxx
  • ihackcode
  • GregMage
  • geekwright
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main security improvements across the changeset: XSS mitigation, eval() removal, password reset insecurity fixes, and null guard additions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses remaining security/migration audit findings across the XOOPS core and bundled modules (XSS hardening, password reset flow, unsafe eval()/unserialize(), and missing null guards), plus removes a PHP 8.2-incompatible vendor demo directory.

Changes:

  • Hardens request handling and output escaping (XSS fixes, POST-only confirmation param, null guards).
  • Removes/locks down dangerous behaviors (eval() in cache, PHP blocks behind XOOPS_ALLOW_PHP_BLOCKS, unserialize() object-injection mitigation).
  • Disables the insecure profile-module lost password flow in favor of the core reset flow; removes WideImage demo files.

Reviewed changes

Copilot reviewed 19 out of 85 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
htdocs/class/cache/file.php Switches cache format defaults and removes eval() read path.
htdocs/class/cache/model.php Hardens cache DB reads with unserialize(..., ['allowed_classes' => false]).
htdocs/class/xoopscomments.php Escapes SCRIPT_NAME to prevent reflected XSS.
htdocs/kernel/block.php Gates PHP block eval() behind XOOPS_ALLOW_PHP_BLOCKS.
htdocs/modules/system/class/block.php Same PHP-block gating for system block class.
htdocs/kernel/object.php Adds allowed_classes => false to array deserialization.
htdocs/kernel/configitem.php Adds allowed_classes => false for config array deserialization.
htdocs/modules/profile/include/update.php Hardens unserialize in profile module update routine.
htdocs/lostpass.php Fixes password field input handling to allow raw/no-trim.
htdocs/modules/profile/preloads/core.php Disables profile lostpass preload redirect to insecure flow.
htdocs/modules/profile/lostpass.php Redirects deprecated profile lostpass endpoint to core lostpass.
htdocs/themes/default/modules/system/system_siteclosed.tpl Escapes REQUEST_URI in hidden redirect field.
htdocs/modules/system/admin/users/main.php Adds null guards; pins delete confirmation ok to POST; uses getText() for HTML-capable fields.
htdocs/include/findusers.php Adds null-guard on rank handler result.
htdocs/include/comment_post.php Uses Request::getText() for HTML-capable comment body.
htdocs/edituser.php Uses Request::getText() for signature/bio to avoid content corruption.
htdocs/xoops_lib/modules/protector/class/gtickets.php Migrates ticket retrieval to Xmf\Request.
htdocs/xoops_lib/modules/protector/filters_disabled/postcommon_register_stopforumspam.php Migrates POST access to Xmf\Request.
htdocs/xoops_lib/vendor/smottt/wideimage/demo/** Deletes demo directory to avoid PHP 8.2 __autoload() fatal and reduce shipped attack surface.
docs/remediation_proposal.md Adds updated remediation proposal documentation.

Comment on lines 307 to 323
@@ -312,7 +319,7 @@
// RMV-NOTIFY
$newuser->setVar('notify_method', Request::getString('notify_method'));
$newuser->setVar('notify_mode', Request::getString('notify_mode'));
$newuser->setVar('bio', Request::getString('bio'));
$newuser->setVar('bio', Request::getText('bio'));
$newuser->setVar('rank', Request::getString('rank'));
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue in the user-create path: Request::getText(...) is called without an explicit hash, so values can come from $_REQUEST instead of POST-only. Pin these inputs to 'POST' (and ideally the rest of the form fields in this POST handler) to prevent cookies/GET parameters from influencing admin-created user profiles.

Copilot uses AI. Check for mistakes.
Comment on lines 361 to +366
if (Request::hasVar('uid')) {
$obj = $member_handler->getUser($uid);
//echo $_REQUEST["uid"];
//print_r($obj);
}
if (!isset($obj) || !is_object($obj)) {
redirect_header('admin.php?fct=users', 2, _AM_SYSTEM_DBUPDATED);
break;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request::hasVar('uid') / $uid resolution here uses the default source ($_REQUEST). For an admin action like activation, the uid should be read from an explicit source (typically GET for a link action, or POST if you later add a confirmation/token) to avoid cookies/other request components influencing which account is activated.

Copilot uses AI. Check for mistakes.
Comment thread htdocs/class/xoopscomments.php Outdated
{
global $xoopsConfig, $xoopsUser;
echo "<form method='get' action='" . $_SERVER['PHP_SELF'] . "'><table width='100%' border='0' cellspacing='1' cellpadding='2'><tr><td class='bg1' align='center'><select name='mode'><option value='nocomments'";
echo "<form method='get' action='" . htmlspecialchars($_SERVER['SCRIPT_NAME'], ENT_QUOTES | ENT_HTML5) . "'><table width='100%' border='0' cellspacing='1' cellpadding='2'><tr><td class='bg1' align='center'><select name='mode'><option value='nocomments'";
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This htmlspecialchars() call omits the charset argument. Elsewhere in the codebase some escaping explicitly uses UTF-8; specifying the encoding here avoids relying on default_charset and prevents edge-case encoding issues. Consider passing 'UTF-8' as the third parameter (and keep ENT_QUOTES).

Copilot uses AI. Check for mistakes.
Comment thread htdocs/class/xoopscomments.php Outdated
$icon = 'subject/' . $this->getVar('icon', 'E');
}
echo "<tr class='$bg' align='left'><td>" . $prefix . "<img src='" . XOOPS_URL . '/images/' . $icon . "'>&nbsp;<a href='" . $_SERVER['PHP_SELF'] . '?item_id=' . $this->getVar('item_id') . '&amp;comment_id=' . $this->getVar('comment_id') . '&amp;mode=' . $mode . '&amp;order=' . $order . '#' . $this->getVar('comment_id') . "'>" . $this->getVar('subject') . "</a></td><td><a href='" . XOOPS_URL . '/userinfo.php?uid=' . $this->getVar('user_id') . "'>" . XoopsUser::getUnameFromId($this->getVar('user_id')) . '</a></td><td>' . $date . '</td></tr>';
echo "<tr class='$bg' align='left'><td>" . $prefix . "<img src='" . XOOPS_URL . '/images/' . $icon . "'>&nbsp;<a href='" . htmlspecialchars($_SERVER['SCRIPT_NAME'], ENT_QUOTES | ENT_HTML5) . '?item_id=' . $this->getVar('item_id') . '&amp;comment_id=' . $this->getVar('comment_id') . '&amp;mode=' . $mode . '&amp;order=' . $order . '#' . $this->getVar('comment_id') . "'>" . $this->getVar('subject') . "</a></td><td><a href='" . XOOPS_URL . '/userinfo.php?uid=' . $this->getVar('user_id') . "'>" . XoopsUser::getUnameFromId($this->getVar('user_id')) . '</a></td><td>' . $date . '</td></tr>';
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: htmlspecialchars() here should include an explicit charset (e.g., UTF-8) rather than relying on default_charset, to ensure consistent escaping behavior across deployments.

Copilot uses AI. Check for mistakes.
'prefix' => 'xoops_',
'lock' => false,
'serialize' => false,
'serialize' => true,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the file-cache default to serialized payloads will make existing cache files (previously written as return ...; when serialize defaulted to false) unreadable. On upgrade, read() will attempt unserialize() on legacy payloads, causing warnings and cache misses until the cache directory is cleared. Consider adding a backwards-compat/auto-invalidation path (e.g., detect legacy return payloads or delete the cache file when unserialize() fails) so upgrades don’t flood logs or require manual cache clears.

Suggested change
'serialize' => true,
'serialize' => false,

Copilot uses AI. Check for mistakes.
Comment on lines 212 to 224
if (!empty($data) && !empty($this->settings['serialize'])) {
$data = stripslashes($data);
// $data = preg_replace('!s:(\d+):"(.*?)";!se', "'s:'.strlen('$2').':\"$2\";'", $data);
$data = preg_replace_callback('!s:(\d+):"(.*?)";!s', function ($m) { return 's:' . strlen($m[2]) . ':"' . $m[2] . '";'; }, $data);
$data = unserialize($data, ['allowed_classes' => false]);
if (is_array($data)) {
XoopsLoad::load('XoopsUtility');
$data = XoopsUtility::recursive('stripslashes', $data);
}
} elseif ($data && empty($this->settings['serialize'])) {
$data = eval($data);
// eval() removed for security — cache data must use serialization
$data = null;
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write() still supports the non-serialized mode by writing PHP code via var_export(...), but read() now hard-returns null when serialize is disabled. This creates an inconsistent configuration where the cache will silently never return values if someone sets serialize to false. Either remove/support the legacy non-serialized mode consistently (e.g., force serialize=true in init with a warning, or update write() to match the new constraint).

Copilot uses AI. Check for mistakes.
Comment thread htdocs/kernel/configitem.php Outdated
break;
case 'array':
$value = @unserialize($this->getVar('conf_value', 'N'));
$value = unserialize($this->getVar('conf_value', 'N'), ['allowed_classes' => false]);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unserialize() will emit warnings (and can return false) when conf_value is empty or corrupted. Previously this path suppressed warnings; now it doesn’t, and there’s no guard before calling unserialize(). To avoid noisy logs/user-visible warnings, add an empty-string check and/or handle failed unserialize results explicitly (still using allowed_classes => false).

Suggested change
$value = unserialize($this->getVar('conf_value', 'N'), ['allowed_classes' => false]);
$confValue = $this->getVar('conf_value', 'N');
if ($confValue === '' || $confValue === null) {
return [];
}
$value = unserialize($confValue, ['allowed_classes' => false]);

Copilot uses AI. Check for mistakes.
Comment on lines 210 to 215
$edituser->setVar('url', formatURL(Request::getUrl('url')));
$edituser->setVar('user_icq', Request::getString('user_icq'));
$edituser->setVar('user_from', Request::getString('user_from'));
$edituser->setVar('user_sig', Request::getString('user_sig'));
$edituser->setVar('user_sig', Request::getText('user_sig'));
$edituser->setVar('user_viewemail', (int)(Request::getInt('user_viewemail', 0) == 1));
$edituser->setVar('user_aim', Request::getString('user_aim'));
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These updated fields now use Request::getText(...) without specifying the input source, so they’ll read from $_REQUEST (GET+POST+COOKIE). For admin user updates this should be pinned to POST to avoid cookies/GET overriding form submissions; update these calls (and the surrounding Request::getString/getInt/getUrl calls in this POST handler) to pass 'POST' explicitly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/remediation_proposal.md`:
- Around line 168-181: Item 3B in the docs is incorrect: the
XOOPS_ALLOW_PHP_BLOCKS guard around eval() is already implemented in
htdocs/kernel/block.php and htdocs/modules/system/class/block.php (they check
XOOPS_ALLOW_PHP_BLOCKS, log a warning via XoopsLogger::addWarning, and return ''
when disabled); update the remediation_proposal.md entry to mark this item as
"Already Fixed" and move it to the fixed section, or alternatively reframe the
recommendation to remove eval() entirely (mentioning the symbols
XOOPS_ALLOW_PHP_BLOCKS and the eval() call in those two files) if you want a
stricter remediation rather than the existing guard.
- Around line 33-42: Markdown code blocks in docs/remediation_proposal.md around
the shown snippet are missing surrounding blank lines causing rendering issues;
add a blank line before and after each fenced code block shown (the block
containing Request::getString calls for $pass and $vpass and the blocks at the
other noted ranges) and update the example fix block so the example uses
Request::getVar for $pass and $vpass with the raw flags (Request::MASK_ALLOW_RAW
| Request::MASK_NO_TRIM) as described; ensure every fenced block has one empty
line above and below to match the other fixed files and the example sections at
lines 33-42, 90-92, and 140-149.

In `@htdocs/class/cache/file.php`:
- Line 110: The change flips the cache 'serialize' default to true which breaks
reading legacy files written with serialize=false (var_export format); update
the file cache implementation so the read() method first attempts unserialize(),
and if that returns null/fails, fall back to parsing legacy var_export() output
(safely evaluate or implement a parser) before treating it as a miss, and also
add an upgrade/migration note telling administrators to clear the file cache
after upgrading; refer to the 'serialize' config key and the class's read() (and
write()) behavior when making the fix.

In `@htdocs/edituser.php`:
- Line 103: The bio field is being truncated using
substr(Request::getText('bio', '', 'POST'), 0, 255) which can break multibyte
characters; replace the substr call with xoops_substr to safely handle multibyte
strings (i.e., change the truncation around Request::getText('bio', '', 'POST')
to xoops_substr(..., 0, 255)) so the bio is consistently truncated without
corrupting multibyte characters.

In `@htdocs/include/findusers.php`:
- Around line 570-580: The current guard uses is_object($rank_obj) after calling
XoopsRankHandler::get(Request::getInt('rank', 0, 'POST')), but
XoopsRankHandler::get returns a placeholder XoopsRank object for missing rows so
is_object() does not prove the rank exists; change the logic to skip the lookup
when Request::getInt('rank', 0, 'POST') === 0 OR, after fetching $rank_obj,
verify a real id (e.g. null !== $rank_obj->getVar('rank_id') or
(int)$rank_obj->getVar('rank_id') > 0) before using getVar('rank_special'),
getVar('rank_min') and getVar('rank_max') so invalid/placeholder ranks are
ignored.

In `@htdocs/modules/system/admin/users/main.php`:
- Line 87: The literal 'admin.php?fct=users' is duplicated; introduce a local
variable (e.g. $usersAdminUrl or $usersAdminPath) before the switch that builds
the return path and replace the hard-coded string in redirect_header(...) and
any other occurrences inside the switch with that variable to eliminate the
duplicate literal and reuse a single constant value.
- Around line 360-369: The users_active branch currently trusts a request
parameter and flips user level based only on Request::hasVar('uid'); change it
to require a POST with a valid token like the delete flow: check
Request::getMethod() is 'POST' (or Request::hasVar with REQUEST_METHOD), verify
the XOOPS security token (or the same token check used in the delete path), and
only then call $member_handler->getUser($uid), $obj->setVar('level', 1), and
$member_handler->insertUser($obj, true); if the token fails or method is not
POST, redirect or show an error (similar to redirect_header usage) and do not
mutate the user.
- Around line 86-88: The redirect when a user lookup fails currently reports
success (uses redirect_header with _AM_SYSTEM_DBUPDATED) but should report a
missing-user error: replace the success constant with
_AM_SYSTEM_USERS_NO_SUCH_USER when $user is not found (the guard checking
!is_object($user) and any similar single-user branches referencing $user). Also
update the batch-delete/edit branch that iterates over IDs (the code that
silently skips missing IDs) to detect missing IDs, track a partial-failure flag,
and redirect with an appropriate message (use _AM_SYSTEM_USERS_NO_SUCH_USER for
complete misses or a partial-failure message when some IDs succeed and some
fail) instead of always using _AM_SYSTEM_DBUPDATED; ensure all places that
currently call redirect_header(..., _AM_SYSTEM_DBUPDATED) for missing users are
changed (look for usages of $user, redirect_header, and the constants
_AM_SYSTEM_DBUPDATED/_AM_SYSTEM_USERS_NO_SUCH_USER).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b5f293a-617f-49e0-aa0b-53eba4f0de5f

📥 Commits

Reviewing files that changed from the base of the PR and between 0275736 and 8cc8a1b.

⛔ Files ignored due to path filters (24)
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/bg.gif is excluded by !**/*.gif
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/Vera.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraBI.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraBd.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraIt.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraMoBI.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraMoBd.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraMoIt.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraMono.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraSe.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/VeraSeBd.ttf is excluded by !**/*.ttf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/1-rainbow.png is excluded by !**/*.png
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/2-blue-alpha.png is excluded by !**/*.png
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/3-smiley.gif is excluded by !**/*.gif
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/4-color-hole.gif is excluded by !**/*.gif
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/5-circle.png is excluded by !**/*.png
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/6-logo.gif is excluded by !**/*.gif
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/7-overlay.png is excluded by !**/*.png
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/bg03.bmp is excluded by !**/*.bmp
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/images/fgnl.jpg is excluded by !**/*.jpg
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/masks/mask-circle.gif is excluded by !**/*.gif
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/masks/mask-circle.png is excluded by !**/*.png
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/masks/mask-diagonal.gif is excluded by !**/*.gif
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/masks/mask-smiley.gif is excluded by !**/*.gif
📒 Files selected for processing (61)
  • docs/remediation_proposal.md
  • htdocs/class/cache/file.php
  • htdocs/class/cache/model.php
  • htdocs/class/xoopscomments.php
  • htdocs/edituser.php
  • htdocs/include/comment_post.php
  • htdocs/include/findusers.php
  • htdocs/kernel/block.php
  • htdocs/kernel/configitem.php
  • htdocs/kernel/object.php
  • htdocs/lostpass.php
  • htdocs/modules/profile/include/update.php
  • htdocs/modules/profile/lostpass.php
  • htdocs/modules/profile/preloads/core.php
  • htdocs/modules/system/admin/users/main.php
  • htdocs/modules/system/class/block.php
  • htdocs/themes/default/modules/system/system_siteclosed.tpl
  • htdocs/xoops_lib/modules/protector/class/gtickets.php
  • htdocs/xoops_lib/modules/protector/filters_disabled/postcommon_register_stopforumspam.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demo_screen.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/addNoise.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/applyConvolution.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/applyFilter.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/applyMask.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/asGrayscale.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/asNegative.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/autoCrop.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/correctGamma.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/crop.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/flip.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/getCanvas.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/getChannels.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/getMask.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/merge.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/mirror.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/resize.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/resizeCanvas.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/rotate.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/roundCorners.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/unsharp.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/font.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/COPYRIGHT.TXT
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/README.TXT
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/RELEASENOTES.TXT
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/local.conf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/AngleField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/CheckboxField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/CheckboxSetField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/ColorField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/CoordinateField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/Demo.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/Field.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/FileSelectField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/FloatField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/FormatSelectField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/IntField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/Request.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/SelectField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/common.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/image.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/index.php
💤 Files with no reviewable changes (42)
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/addNoise.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/getMask.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/correctGamma.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demo_screen.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/asGrayscale.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/FloatField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/applyFilter.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/image.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/unsharp.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/Field.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/COPYRIGHT.TXT
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/CheckboxSetField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/roundCorners.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/RELEASENOTES.TXT
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/common.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/SelectField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/crop.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/index.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/getCanvas.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/resizeCanvas.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/asNegative.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/CheckboxField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/flip.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/merge.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/mirror.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/FileSelectField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/resize.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/Demo.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/rotate.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/FormatSelectField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/autoCrop.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/README.TXT
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/getChannels.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/fonts/local.conf
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/applyConvolution.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/AngleField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/Request.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/demos/applyMask.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/CoordinateField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/font.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/IntField.php
  • htdocs/xoops_lib/vendor/smottt/wideimage/demo/helpers/ColorField.php

Comment thread docs/remediation_proposal.md Outdated
Comment thread docs/remediation_proposal.md Outdated
'prefix' => 'xoops_',
'lock' => false,
'serialize' => false,
'serialize' => true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Breaking change: Serialize default flipped from false to true.

This change affects backward compatibility. Existing cache files written with serialize=false use PHP's var_export() format. After this change, read() will attempt unserialize() on that data, which will fail and return null.

Affected callers using default settings include:

  • htdocs/image.php (edited image cache)
  • htdocs/admin.php (RSS items cache)
  • htdocs/class/xoopsform/formselectuser.php (user list cache)
  • htdocs/class/LostPassSecurity.php (password reset tokens)

All existing cache entries will effectively be invalidated after upgrade—they'll be treated as cache misses until they naturally expire or are regenerated.

Recommendation: Add upgrade/migration notes instructing administrators to clear the file cache after applying this update. This ensures a clean slate and prevents silent failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/class/cache/file.php` at line 110, The change flips the cache
'serialize' default to true which breaks reading legacy files written with
serialize=false (var_export format); update the file cache implementation so the
read() method first attempts unserialize(), and if that returns null/fails, fall
back to parsing legacy var_export() output (safely evaluate or implement a
parser) before treating it as a miss, and also add an upgrade/migration note
telling administrators to clear the file cache after upgrading; refer to the
'serialize' config key and the class's read() (and write()) behavior when making
the fix.

Comment thread htdocs/edituser.php Outdated
Comment thread htdocs/include/findusers.php Outdated
Comment thread htdocs/modules/system/admin/users/main.php
$user = $member_handler->getUser($uid);
if ((int) Request::getInt('ok', 0) === 1) {
if (!is_object($user)) {
redirect_header('admin.php?fct=users', 2, _AM_SYSTEM_DBUPDATED);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Extract the users-admin return path.

This branch adds another admin.php?fct=users occurrence to a file that already fails Sonar on that duplicated literal. Hoist it into a local constant/variable and reuse it across the switch.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 87-87: Define a constant instead of duplicating this literal "admin.php?fct=users" 14 times.

See more on https://sonarcloud.io/project/issues?id=XOOPS_XoopsCore25&issues=AZzNDiDabUJJ94dz4NHa&open=AZzNDiDabUJJ94dz4NHa&pullRequest=1636

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/users/main.php` at line 87, The literal
'admin.php?fct=users' is duplicated; introduce a local variable (e.g.
$usersAdminUrl or $usersAdminPath) before the switch that builds the return path
and replace the hard-coded string in redirect_header(...) and any other
occurrences inside the switch with that variable to eliminate the duplicate
literal and reuse a single constant value.

Comment on lines 360 to 369
case 'users_active':
if (Request::hasVar('uid')) {
$obj = $member_handler->getUser($uid);
//echo $_REQUEST["uid"];
//print_r($obj);
}
if (!isset($obj) || !is_object($obj)) {
redirect_header('admin.php?fct=users', 2, _AM_SYSTEM_DBUPDATED);
break;
}
$obj->setVar('level', 1);
if ($member_handler->insertUser($obj, true)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Protect users_active with the same POST+token flow as delete.

This branch still flips level from a request parameter alone. An attacker only needs an authenticated admin to follow a crafted request to activate an account. Require a token-backed POST here before mutating the user record.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/users/main.php` around lines 360 - 369, The
users_active branch currently trusts a request parameter and flips user level
based only on Request::hasVar('uid'); change it to require a POST with a valid
token like the delete flow: check Request::getMethod() is 'POST' (or
Request::hasVar with REQUEST_METHOD), verify the XOOPS security token (or the
same token check used in the delete path), and only then call
$member_handler->getUser($uid), $obj->setVar('level', 1), and
$member_handler->insertUser($obj, true); if the token fails or method is not
POST, redirect or show an error (similar to redirect_header usage) and do not
mutate the user.

mambax7 and others added 2 commits March 8, 2026 06:59
Fixes Scrutinizer warning about undeclared constant by using the
defined('X') ? constant('X') : fallback pattern per project conventions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
   - Add UTF-8 charset param to htmlspecialchars() in xoopscomments.php
   - Add empty-string guard before unserialize() in configitem.php
   - Use xoops_substr() for bio truncation in edituser.php (multibyte safe)
   - Use _AM_SYSTEM_USERS_NO_SUCH_USER instead of _AM_SYSTEM_DBUPDATED in null-guard redirects for delete and activate flows
   - Pin getText() calls for user_sig and bio to POST in admin users
   - Improve rank guard in findusers.php: skip lookup when rank==0 and verify rank_id > 0 to handle placeholder objects
   - Revert cache serialize default to false for backward compatibility; replace eval() with safe include-based parsing of legacy var_export cache format
   - Use defined/constant pattern for XOOPS_ALLOW_PHP_BLOCKS to satisfy Scrutinizer static analysis
   - Update remediation_proposal.md: mark 3B as fixed, fix markdown formatting
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
htdocs/modules/system/admin/users/main.php (1)

360-373: ⚠️ Potential issue | 🟠 Major

Null guard added, but CSRF protection still missing.

The null guard at lines 364-366 is a good defensive addition. However, this users_active branch still modifies user state based solely on request parameters without CSRF token validation—unlike the users_delete branch which properly checks $GLOBALS['xoopsSecurity']->check().

An attacker can activate accounts by tricking an admin into following a crafted link.

🛡️ Proposed fix: Add CSRF check
     case 'users_active':
+        if (!$GLOBALS['xoopsSecurity']->check()) {
+            redirect_header('admin.php?fct=users', 3, implode('<br>', $GLOBALS['xoopsSecurity']->getErrors()));
+            break;
+        }
         if (Request::hasVar('uid')) {
             $obj = $member_handler->getUser($uid);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/users/main.php` around lines 360 - 373, The
users_active branch lacks CSRF protection; add the same security check used in
the users_delete flow by invoking $GLOBALS['xoopsSecurity']->check() before
modifying the user ($obj->setVar and $member_handler->insertUser) and if the
check fails, call redirect_header with an appropriate error message (e.g.
_AM_SYSTEM_DBUPDATED or a security token error) to halt the action; ensure this
check runs after Request::hasVar('uid') and before calling
$member_handler->insertUser($obj, true).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/remediation_proposal.md`:
- Around line 235-247: The document's summary numbers are inconsistent;
reconcile the totals by updating the phrasing block containing "Total remaining:
13 fixes across ~16 files. 9 items already fixed, 4 deferred." to match the
other summary/count statements (the line that currently reads "22 items fixed, 4
deferred" and the per-phase item counts). Recalculate remaining fixes from the
per-phase counts (or update the "22 items fixed"/"9 items already fixed" phrase
so all three numbers—remaining, fixed, deferred—sum to the same initial total)
and edit the two inconsistent snippets so the table, the “Total remaining…”
line, and the “items fixed/deferred” text all agree.
- Around line 26-220: Convert the Phase 1–5 sections (including items
1A-remaining, 1B, 1C, 2A–2E, 3A–3C, 4B, 5A) from an open remediation plan into a
post-remediation record by changing "STILL EXISTS" statuses to "Fixed in this
PR" where fixes were merged, replacing proposed-fix prose with the actual
implementation details applied (e.g., for 1A use Request::getVar with raw flags;
for 1B stop the preload redirect in modules/profile/preloads/core.php and
redirect modules/profile/lostpass.php to XOOPS_URL . '/lostpass.php'; for null
guards in system/admin/users/main.php add the is_object checks; for 2A/2B use
SCRIPT_NAME/escape; for 2C change getString→getText; for 2D pin ok to POST; for
2E add is_object guard; for 3A switch file cache to serialize/json_decode; for
3C use unserialize(..., ['allowed_classes'=>false]; for 4B remove demo dir; for
5A replace raw superglobals with Request APIs), and move any remaining true
deferred items into a separate "Follow-up" section or a separate document with
clear references to the files and TODOs.

In `@htdocs/class/cache/file.php`:
- Around line 222-235: The include of the temp file can emit PHP
warnings/notices on invalid syntax; wrap the include $tempFile call in a
temporary error handler that converts warnings into exceptions (or suppress
errors and check for a false return) so parsing failures fail gracefully and you
can set $data = null on error, then restore the original error handler; adjust
the block around $tempFile, include $tempFile and unlink($tempFile) to ensure
the temp file is always removed even on error (use finally-like cleanup via
try/catch or error-handler + careful control flow).

In `@htdocs/include/findusers.php`:
- Around line 569-584: The rank lookup should use strict typing: after fetching
$rankId via Request::getInt and obtaining $rank_obj with
$rank_handler->get($rankId), replace the is_object($rank_obj) check with an
instanceof check (i.e., $rank_obj instanceof XoopsRank) while keeping the
existing getVar('rank_id') > 0 guard and the subsequent handling of
rank_special, rank_min and rank_max; this ensures only real XoopsRank instances
are accepted and preserves the current logic.

---

Duplicate comments:
In `@htdocs/modules/system/admin/users/main.php`:
- Around line 360-373: The users_active branch lacks CSRF protection; add the
same security check used in the users_delete flow by invoking
$GLOBALS['xoopsSecurity']->check() before modifying the user ($obj->setVar and
$member_handler->insertUser) and if the check fails, call redirect_header with
an appropriate error message (e.g. _AM_SYSTEM_DBUPDATED or a security token
error) to halt the action; ensure this check runs after Request::hasVar('uid')
and before calling $member_handler->insertUser($obj, true).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ddf21cc-62d6-4682-b977-8fd3051f681f

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc8a1b and ebb323c.

📒 Files selected for processing (9)
  • docs/remediation_proposal.md
  • htdocs/class/cache/file.php
  • htdocs/class/xoopscomments.php
  • htdocs/edituser.php
  • htdocs/include/findusers.php
  • htdocs/kernel/block.php
  • htdocs/kernel/configitem.php
  • htdocs/modules/system/admin/users/main.php
  • htdocs/modules/system/class/block.php

Comment thread docs/remediation_proposal.md Outdated
Comment thread docs/remediation_proposal.md Outdated
Comment thread htdocs/class/cache/file.php Outdated
Comment on lines +569 to 584
$rankId = Request::getInt('rank', 0, 'POST');
if ($rankId > 0) {
$rank_obj = $rank_handler->get($rankId);
if (is_object($rank_obj) && (int) $rank_obj->getVar('rank_id') > 0) {
if ($rank_obj->getVar('rank_special')) {
$criteria->add(new Criteria('rank', $rankId));
} else {
if ($rank_obj->getVar('rank_min')) {
$criteria->add(new Criteria('posts', $rank_obj->getVar('rank_min'), '>='));
}
if ($rank_obj->getVar('rank_max')) {
$criteria->add(new Criteria('posts', $rank_obj->getVar('rank_max'), '<='));
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Well-implemented rank validation fix.

This change properly addresses the placeholder-object problem from the previous review. The guard now:

  1. Extracts $rankId upfront and skips the lookup entirely when $rankId === 0
  2. Validates the fetched object has an actual rank_id > 0, catching the case where get() returns an uninitialized XoopsRank placeholder

One minor type-safety improvement: consider using $rank_obj instanceof XoopsRank instead of is_object($rank_obj) for stricter type checking, though the current implementation is functionally correct given the rank_id > 0 check.

Optional refinement for stricter typing
     $rankId = Request::getInt('rank', 0, 'POST');
     if ($rankId > 0) {
         $rank_obj = $rank_handler->get($rankId);
-        if (is_object($rank_obj) && (int) $rank_obj->getVar('rank_id') > 0) {
+        if ($rank_obj instanceof XoopsRank && (int) $rank_obj->getVar('rank_id') > 0) {
             if ($rank_obj->getVar('rank_special')) {

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/include/findusers.php` around lines 569 - 584, The rank lookup should
use strict typing: after fetching $rankId via Request::getInt and obtaining
$rank_obj with $rank_handler->get($rankId), replace the is_object($rank_obj)
check with an instanceof check (i.e., $rank_obj instanceof XoopsRank) while
keeping the existing getVar('rank_id') > 0 guard and the subsequent handling of
rank_special, rank_min and rank_max; this ensures only real XoopsRank instances
are accepted and preserves the current logic.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 85 changed files in this pull request and generated 3 comments.

Comment thread htdocs/class/cache/file.php Outdated
Comment on lines +221 to +229
} elseif ($data && empty($this->settings['serialize'])) {
$data = eval($data);
// Legacy non-serialized cache uses var_export() format: "return <value>;"
// eval() removed for security — attempt to parse the var_export output safely
if (str_starts_with(trim($data), 'return ') && str_ends_with(trim($data), ';')) {
$tempFile = tempnam(sys_get_temp_dir(), 'xoops_cache_');
if ($tempFile !== false) {
file_put_contents($tempFile, '<?php ' . $data);
$data = include $tempFile;
unlink($tempFile);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “legacy non-serialized cache” path still executes PHP by writing cache contents into a temporary PHP file and includeing it. This is effectively equivalent to the previous eval() behavior (still code execution if the cache file contents are ever influenced/tampered with), and in this file the default serialize setting is still false, so this execution path remains the default. Safer options are: (1) switch the engine default to serialize => true and remove/disable the non-serialized branch entirely (return null/false for legacy entries), or (2) implement a non-executing parser limited to scalars/arrays and reject any non-conforming content (including objects).

Copilot uses AI. Check for mistakes.
Comment on lines 131 to 135
if (Request::hasVar('memberslist_id')) {
$xoBreadCrumb->render();
$error = '';
foreach (Request::getArray('memberslist_id', []) as $del) {
$del = (int) $del;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memberslist_id is being read from the default request hash (i.e., $_REQUEST) because Request::hasVar('memberslist_id') and Request::getArray('memberslist_id', []) omit the explicit source. For admin bulk actions (and to match the superglobal-migration hardening elsewhere in this PR), this should be pinned to POST to avoid accepting the same parameters via GET/COOKIE.

Copilot uses AI. Check for mistakes.
Comment thread htdocs/kernel/configitem.php Outdated
Comment on lines 202 to 204
$value = unserialize($confValue, ['allowed_classes' => false]);

return $value ?: [];
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unserialize() here is no longer error-suppressed, so a corrupted or non-serialized conf_value for an array-typed config will emit warnings/notices at runtime. Consider handling invalid serialized payloads explicitly (e.g., temporary error handler around unserialize() or validating the prefix like a:/s:/etc.) so this accessor can safely return [] without generating output/log noise.

Suggested change
$value = unserialize($confValue, ['allowed_classes' => false]);
return $value ?: [];
if (!is_string($confValue)) {
return [];
}
$previousErrorHandler = set_error_handler(static function () {
// Suppress warnings/notices from unserialize() on invalid payloads
return true;
});
try {
$value = unserialize($confValue, ['allowed_classes' => false]);
} catch (\Throwable $throwable) {
$value = false;
} finally {
if ($previousErrorHandler !== null) {
set_error_handler($previousErrorHandler);
} else {
restore_error_handler();
}
}
// Treat invalid or non-array payloads as empty array
if ($value === false && $confValue !== 'b:0;') {
return [];
}
if (!is_array($value)) {
return [];
}
return $value;

Copilot uses AI. Check for mistakes.
  - Add UTF-8 charset to htmlspecialchars() in xoopscomments.php
   - configitem.php: fix @return to mixed, add type/empty guard before
     unserialize(), use set_error_handler() for corrupted data instead
     of @ suppression
   - edituser.php: use xoops_substr() for bio (multibyte safe)
   - system admin users: use _AM_SYSTEM_USERS_NO_SUCH_USER in null-guard
     redirects, pin getText() for user_sig/bio to POST
   - findusers.php: skip rank lookup when rankId==0, verify rank_id > 0
     to handle placeholder objects from handler->get()
   - cache/file.php: revert to serialize=>true default and remove legacy
     eval/include path entirely — treat non-serialized entries as cache
     miss (regenerated on next write)
   - block.php: use defined()/constant() pattern for XOOPS_ALLOW_PHP_BLOCKS
     to satisfy Scrutinizer
   - Rewrite remediation_proposal.md as post-remediation record with
     upgrade notes
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 85 changed files in this pull request and generated 4 comments.

Comment thread htdocs/modules/profile/lostpass.php Outdated

// Deprecated: this endpoint used a weak md5-based reset token.
// All password resets now go through the secure core flow.
include __DIR__ . '/header.php';
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modules/profile/lostpass.php now immediately redirects to the core lostpass flow, but still includes the profile module header first. Since redirect_header() exits, the include is unnecessary overhead and may trigger avoidable side effects (template/theme init, globals) on every hit. Consider redirecting immediately without including the module header.

Suggested change
include __DIR__ . '/header.php';
require_once __DIR__ . '/../../mainfile.php';

Copilot uses AI. Check for mistakes.
Comment on lines 106 to +110
'path' => XOOPS_VAR_PATH . '/caches/xoops_cache',
'extension' => '.php',
'prefix' => 'xoops_',
'lock' => false,
'serialize' => false,
'serialize' => true,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XoopsCacheFile now defaults 'serialize' to true, but the settings documentation in this class still indicates the default is false. Please update the inline docs to reflect the new default so future configuration/maintenance isn’t misled.

Copilot uses AI. Check for mistakes.
Comment thread htdocs/kernel/block.php Outdated
if ($c_type === 'H') {
return str_replace('{X_SITEURL}', XOOPS_URL . '/', $this->getVar('content', 'n'));
} elseif ($c_type === 'P') {
if (!(defined('XOOPS_ALLOW_PHP_BLOCKS') ? constant('XOOPS_ALLOW_PHP_BLOCKS') : false)) {
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XOOPS_ALLOW_PHP_BLOCKS gate treats any non-empty string value (e.g. 'false') as truthy, which can unintentionally enable PHP block execution via misconfiguration. Consider requiring an explicit boolean true (e.g. check that the constant is defined and strictly === true) before allowing the eval() path.

Suggested change
if (!(defined('XOOPS_ALLOW_PHP_BLOCKS') ? constant('XOOPS_ALLOW_PHP_BLOCKS') : false)) {
if (!(defined('XOOPS_ALLOW_PHP_BLOCKS') && constant('XOOPS_ALLOW_PHP_BLOCKS') === true)) {

Copilot uses AI. Check for mistakes.
Comment thread htdocs/modules/system/class/block.php Outdated
if ($c_type === 'H') {
return str_replace('{X_SITEURL}', XOOPS_URL . '/', (string) $this->getVar('content', 'n'));
} elseif ($c_type === 'P') {
if (!(defined('XOOPS_ALLOW_PHP_BLOCKS') ? constant('XOOPS_ALLOW_PHP_BLOCKS') : false)) {
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XOOPS_ALLOW_PHP_BLOCKS gate treats any non-empty string value (e.g. 'false') as truthy, which can unintentionally enable PHP block execution via misconfiguration. Consider requiring an explicit boolean true (e.g. check that the constant is defined and strictly === true) before allowing the eval() path.

Suggested change
if (!(defined('XOOPS_ALLOW_PHP_BLOCKS') ? constant('XOOPS_ALLOW_PHP_BLOCKS') : false)) {
$allowPhpBlocks = defined('XOOPS_ALLOW_PHP_BLOCKS') && constant('XOOPS_ALLOW_PHP_BLOCKS') === true;
if (!$allowPhpBlocks) {

Copilot uses AI. Check for mistakes.
- profile/lostpass.php: use mainfile.php instead of module header for
  redirect-only endpoint (avoids unnecessary template/theme init)
- cache/file.php: update inline doc to reflect serialize default is
  now true
- block.php + system/block.php: require XOOPS_ALLOW_PHP_BLOCKS === true
  (strict boolean) to prevent string 'false' from being truthy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/remediation_proposal.md`:
- Around line 84-87: Add a brief upgrade caveat explaining that password reset
tokens generated before the upgrade are incompatible with the new core flow and
any pending password reset emails will no longer work; instruct admins/users to
request a new password reset if they received a reset link prior to the upgrade,
and reference the related changes (the cache engine default change and
XOOPS_ALLOW_PHP_BLOCKS note) so readers see this as an additional compatibility
item to consider when upgrading.

In `@htdocs/modules/profile/lostpass.php`:
- Around line 20-23: The current deprecated lostpass endpoint immediately
redirects and drops legacy reset query params (id, act, actkey), breaking
in-flight resets; update htdocs/modules/profile/lostpass.php to detect legacy
parameters and either map them into the core flow's expected uid and token
parameters (translate id->uid and construct a temporary token param or forward
act/actkey into uid/token) or intercept and render a clear "please request a new
password reset" page before calling redirect_header; reference the file's
redirect_header call and the core lostpass.php expectation of uid and token when
implementing the parameter translation or user-facing message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32f3e168-9557-4041-8824-40ac8522c518

📥 Commits

Reviewing files that changed from the base of the PR and between ebb323c and fdb3ddd.

📒 Files selected for processing (6)
  • docs/remediation_proposal.md
  • htdocs/class/cache/file.php
  • htdocs/kernel/block.php
  • htdocs/kernel/configitem.php
  • htdocs/modules/profile/lostpass.php
  • htdocs/modules/system/class/block.php

Comment thread docs/remediation_proposal.md
Comment on lines +20 to +23
// Deprecated: this endpoint used a weak md5-based reset token.
// All password resets now go through the secure core flow.
require_once __DIR__ . '/../../mainfile.php';
redirect_header(XOOPS_URL . '/lostpass.php', 0, '', false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

In-flight password resets will fail after upgrade.

The core lostpass.php expects uid and token parameters (per context snippet from htdocs/lostpass.php:52-59), but old reset emails contain id, act, and actkey parameters. Users who received reset emails before upgrade but click the link after upgrade will see a generic redirect rather than the reset form.

Consider documenting this in the upgrade notes as expected behavior, or optionally add parameter translation logic in the core flow to recognize legacy parameters and display a "please request a new reset" message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/profile/lostpass.php` around lines 20 - 23, The current
deprecated lostpass endpoint immediately redirects and drops legacy reset query
params (id, act, actkey), breaking in-flight resets; update
htdocs/modules/profile/lostpass.php to detect legacy parameters and either map
them into the core flow's expected uid and token parameters (translate id->uid
and construct a temporary token param or forward act/actkey into uid/token) or
intercept and render a clear "please request a new password reset" page before
calling redirect_header; reference the file's redirect_header call and the core
lostpass.php expectation of uid and token when implementing the parameter
translation or user-facing message.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 85 changed files in this pull request and generated 5 comments.

$report['ip'] = $_SERVER['REMOTE_ADDR'];
$report['uname'] = $_POST['uname'] ?? null;
$report['uname'] = Request::getString('uname', '', 'POST');
$result = $this->protector->stopForumSpamLookup($report['email'], $report['ip'], $report['uname']);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stopForumSpamLookup() builds the query string by concatenating &email= / &username= without URL-encoding the values. Even after switching to Request::getString(), inputs containing &, = or other special characters can break the request or inject extra query parameters. Encode the email/username (and IP if needed) before calling stopForumSpamLookup(), or update stopForumSpamLookup() to handle encoding internally.

Suggested change
$result = $this->protector->stopForumSpamLookup($report['email'], $report['ip'], $report['uname']);
$encodedEmail = rawurlencode($report['email']);
$encodedIp = rawurlencode($report['ip']);
$encodedUname = rawurlencode($report['uname']);
$result = $this->protector->stopForumSpamLookup($encodedEmail, $encodedIp, $encodedUname);

Copilot uses AI. Check for mistakes.
Comment thread htdocs/kernel/block.php
Comment on lines +341 to +344
if (!(defined('XOOPS_ALLOW_PHP_BLOCKS') && constant('XOOPS_ALLOW_PHP_BLOCKS') === true)) {
$logger = XoopsLogger::getInstance();
$logger->addWarning('PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.');
return '';
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When XOOPS_ALLOW_PHP_BLOCKS is not enabled, this logs a warning on every render of a PHP block. On sites with multiple PHP blocks (or repeated renders), this can spam logs/debug output and add overhead. Consider logging once per request (e.g., via a static guard) or downgrading to a quieter notice mechanism.

Copilot uses AI. Check for mistakes.
Comment on lines +277 to +278
$logger = XoopsLogger::getInstance();
$logger->addWarning('PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.');
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When XOOPS_ALLOW_PHP_BLOCKS is not enabled, this logs a warning on every render of a PHP block. On pages with multiple PHP blocks or frequent renders, this can generate noisy logs/debug output and unnecessary overhead. Consider logging once per request (static guard) or otherwise throttling this warning.

Suggested change
$logger = XoopsLogger::getInstance();
$logger->addWarning('PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.');
static $phpBlockWarningLogged = false;
if (!$phpBlockWarningLogged) {
$logger = XoopsLogger::getInstance();
$logger->addWarning('PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.');
$phpBlockWarningLogged = true;
}

Copilot uses AI. Check for mistakes.
Comment on lines 131 to 136
if (Request::hasVar('memberslist_id')) {
$xoBreadCrumb->render();
$error = '';
foreach (Request::getArray('memberslist_id', []) as $del) {
$del = (int) $del;
$user = $member_handler->getUser($del);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bulk delete reads memberslist_id via Request::getArray('memberslist_id', []) without a source hash, so it defaults to $_REQUEST. Since this action is meant to be driven by a POSTed form submission, consider pinning it to 'POST' to prevent GET/COOKIE values from influencing which accounts are processed.

Copilot uses AI. Check for mistakes.
Comment on lines 221 to 225
} elseif ($data && empty($this->settings['serialize'])) {
$data = eval($data);
// Legacy non-serialized cache used eval() — removed for security.
// Treat as cache miss; entry will be regenerated with serialization on next write.
$data = null;
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XoopsCacheFile::read() no longer evaluates non-serialized cache entries, but write() still supports serialize = false and writes a return ...; payload. With the current change, any consumer configured with serialize = false will write entries that can never be read back (and the legacy serialize=false branch returns null, which doesn’t match the method’s documented false on cache-miss). Consider either removing/ignoring the serialize=false mode (force serialize on), or changing the non-serialized format to something readable without eval() (e.g., a real PHP file you can safely include) and ensure cache-miss returns false consistently.

Copilot uses AI. Check for mistakes.
Pending reset emails sent before upgrade will not work with the new
secure token flow. Users must request a new reset after upgrade.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 85 changed files in this pull request and generated 1 comment.

Comment thread htdocs/kernel/block.php
Comment on lines +342 to +343
$logger = XoopsLogger::getInstance();
$logger->addWarning('PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.');
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XoopsLogger does not define an addWarning() method, so this will trigger a fatal error when PHP blocks are disabled. Use an existing mechanism (e.g. trigger_error(..., E_USER_WARNING) or another XoopsLogger method that exists) and then return an empty string.

Suggested change
$logger = XoopsLogger::getInstance();
$logger->addWarning('PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.');
trigger_error(
'PHP block execution is disabled. Set XOOPS_ALLOW_PHP_BLOCKS to true in mainfile.php to enable.',
E_USER_WARNING
);

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/remediation_proposal.md`:
- Around line 11-55: The summary lists 22 fixes but the table shows 15 grouped
rows, so add a crosswalk that maps each of the original 22 audit finding IDs to
the grouped rows (or annotate each grouped row with a per-row count and the list
of original IDs); update the Phase tables (rows like 1A, 1B, 1C, 2A–2E, 3A–3C,
4A–4C, 5A) to include either "(n findings: ID1, ID2, ...)" or append the exact
original audit IDs that were collapsed into that row (e.g., list the six file
IDs under 1A for getString() -> Request::getVar(), the three system admin
instances under 1C for getUser() null guards, the five unserialize() instances
under 3C, etc.) so an auditor can reconstruct the total of 22 from the document.
- Line 86: The docs currently hardcode the cache path
"xoops_data/caches/xoops_cache/" in the upgrade note about the cache default
change (serialize => true); update this sentence to avoid a literal path and
instead instruct admins to clear their configured cache directory (referencing
the installation variable/constant used by XOOPS, e.g. the configured
var/trust/cache path or the project constant such as XOOPS_VAR_PATH or
XOOPS_CACHE_PATH), and give a short example phrased generically (like "clear
your configured cache directory or the cache path defined by your XOOPS
installation/constant") so readers are directed to the correct location.
- Around line 74-79: Update the "Files Changed" table to remove ellipses and use
the exact repository paths: replace `themes/default/.../system_siteclosed.tpl`
with `themes/default/modules/system/system_siteclosed.tpl` and replace
`xoops_lib/.../postcommon_register_stopforumspam.php` with
`xoops_lib/modules/protector/filters_disabled/postcommon_register_stopforumspam.php`;
ensure these exact filenames are used wherever the table lists file paths (e.g.,
in the rows for "XSS fixes" and "Superglobals") so all entries are fully
qualified and grep-able.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17a194ac-392d-4246-9948-10c342f51c0b

📥 Commits

Reviewing files that changed from the base of the PR and between fdb3ddd and e4ebb06.

📒 Files selected for processing (1)
  • docs/remediation_proposal.md

Comment thread docs/remediation_proposal.md Outdated
Comment on lines +11 to +55
Two independent audits identified **26 distinct issues** across security, compatibility, and correctness categories. **22 have been fixed** across two rounds of commits. 4 items are deferred as intentional design or already correct.

---

## Fixed Items

### Phase 1: Critical

| Item | What was fixed | How |
|------|----------------|-----|
| 1A | Password fields using `getString()` (all 6 files) | Changed to `Request::getVar()` with `MASK_ALLOW_RAW \| MASK_NO_TRIM` |
| 1B | Insecure profile lostpass (md5 5-char token) | Disabled preload redirect in `core.php`; `profile/lostpass.php` now redirects to secure core flow |
| 1C | Missing null guards after `getUser()` in system admin (3 instances) | Added `!is_object()` guards with `_AM_SYSTEM_USERS_NO_SUCH_USER` redirect |

### Phase 2: High

| Item | What was fixed | How |
|------|----------------|-----|
| 2A | Reflected XSS via `$_SERVER['PHP_SELF']` in `xoopscomments.php` | Replaced with `htmlspecialchars($_SERVER['SCRIPT_NAME'], ENT_QUOTES \| ENT_HTML5, 'UTF-8')` |
| 2B | Raw `REQUEST_URI` in `system_siteclosed.tpl` | Added `\|escape` Smarty modifier |
| 2C | Content fields (`com_text`, `user_sig`, `bio`) using `getString()` | Changed to `getText()` pinned to `'POST'` |
| 2D | CSRF: `ok` parameter not pinned to POST | Changed to `Request::getInt('ok', 0, 'POST')` |
| 2E | Missing null guards in `findusers.php` and profile admin files | Added `is_object()` + `rank_id > 0` check; profile admin files already fixed |

### Phase 3: Security Hardening

| Item | What was fixed | How |
|------|----------------|-----|
| 3A | `eval()` in file cache engine | Removed `eval()` path; changed default `serialize` to `true`; legacy non-serialized entries treated as cache miss |
| 3B | `eval()` for custom PHP blocks | Gated behind `XOOPS_ALLOW_PHP_BLOCKS` constant (default `false`) using `defined()/constant()` pattern |
| 3C | Unsafe `unserialize()` (5 instances) | Added `['allowed_classes' => false]` to all calls; `configitem.php` uses `set_error_handler()` for corrupted data |

### Phase 4: Compatibility

| Item | What was fixed | How |
|------|----------------|-----|
| 4A | Smarty `{php}` tags in imagemanager templates | Removed `{php}` blocks (fixed in prior commits) |
| 4B | PHP 8.2 fatal in vendor demo (`__autoload()`) | Deleted `wideimage/demo/` directory |
| 4C | Type safety for `$groups_failed` in SQL | Already cast to `(int)` in loop (confirmed correct) |

### Phase 5: Consistency

| Item | What was fixed | How |
|------|----------------|-----|
| 5A | Raw superglobals in `gtickets.php` and `stopforumspam.php` | Migrated to `Xmf\Request::getString()` / `hasVar()` |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a crosswalk from the 22 audit findings to these 15 grouped rows.

The summary says 22 findings were fixed, but this section only exposes 15 line items. Because several rows collapse multiple instances (1A, 1C, 3C, etc.), an auditor cannot reconstruct the closure count from this record alone. Please add the original audit IDs or a per-row finding count so the totals are traceable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/remediation_proposal.md` around lines 11 - 55, The summary lists 22
fixes but the table shows 15 grouped rows, so add a crosswalk that maps each of
the original 22 audit finding IDs to the grouped rows (or annotate each grouped
row with a per-row count and the list of original IDs); update the Phase tables
(rows like 1A, 1B, 1C, 2A–2E, 3A–3C, 4A–4C, 5A) to include either "(n findings:
ID1, ID2, ...)" or append the exact original audit IDs that were collapsed into
that row (e.g., list the six file IDs under 1A for getString() ->
Request::getVar(), the three system admin instances under 1C for getUser() null
guards, the five unserialize() instances under 3C, etc.) so an auditor can
reconstruct the total of 22 from the document.

Comment thread docs/remediation_proposal.md Outdated
Comment thread docs/remediation_proposal.md Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 85 changed files in this pull request and generated 1 comment.


$report = [];
$report['email'] = $_POST['email'] ?? null;
$report['email'] = Request::getString('email', '', 'POST');
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since $report['email'] is expected to be an email address (and is later concatenated into an HTTP query string), it should be retrieved/validated as an email rather than a generic string. Consider using Request::getEmail('email', '', 'POST') here (and possibly urlencode() when building the stopforumspam URL in stopForumSpamLookup()) to avoid malformed input affecting the outbound request.

Suggested change
$report['email'] = Request::getString('email', '', 'POST');
$report['email'] = Request::getEmail('email', '', 'POST');

Copilot uses AI. Check for mistakes.
…e path

- Add per-item instance counts and file lists so auditors can
  reconstruct the total of 22 from the grouped table rows
- Use full repository paths instead of ellipses in Files Changed table
- Reference XOOPS_VAR_PATH instead of hardcoded cache path in
  upgrade notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 8, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
13.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/remediation_proposal.md`:
- Around line 17-56: Phase headers are inconsistent: Phase 4 header ("Phase 4:
Compatibility") omits an "instances" count and Phase 5 uses "1 item, 2 files"
instead of "instances"; update the Phase 4 and Phase 5 headers to match the "X
items, Y instances" format used in Phases 1–3 (e.g., change Phase 4 to "3 items,
3 instances" or to the correct instance count, and change Phase 5 to "1 item, 2
instances"), and add a short parenthetical note under Phase 4 if you
intentionally count files/directories instead of instances to document the
methodology; modify the markdown headers and the summary table entries
(referencing "Phase 4: Compatibility" and "Phase 5: Consistency") so all
phase-level counts follow the same standardized phrasing.
- Around line 1-14: The remediation record references missing source audit files
(security-critical-audit-2026-03-08.md and migration_audit_findings.md) so
auditors cannot verify the claimed "22 actionable items fixed" or the mapping to
grouped items (1A–5A); fix by either adding the original audit documents to the
repo, or adding a crosswalk table in docs/remediation_proposal.md that maps each
original finding ID to the corresponding grouped remediation item, or by
annotating each grouped row (1A–5A) with the specific original finding IDs it
addresses; ensure the chosen change clearly lists each original finding ID and
the remediation commit/PR references so traceability is restored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4cf0d0bc-f90a-4abe-9e01-f47c59bb7708

📥 Commits

Reviewing files that changed from the base of the PR and between e4ebb06 and 80c2008.

📒 Files selected for processing (1)
  • docs/remediation_proposal.md

Comment on lines +1 to +14
# XOOPS Core 2.5 — Security Audit Remediation Record

**Date:** 2026-03-08 (final)
**Based on:** `security-critical-audit-2026-03-08.md` + `migration_audit_findings.md`
**Status:** All 22 actionable items fixed. 4 items deferred as acceptable.

---

## Executive Summary

Two independent audits identified **26 distinct issues** across security, compatibility, and correctness categories. **22 have been fixed** across two rounds of commits. 4 items are deferred as intentional design or already correct.

---

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find the original audit documents and extract finding identifiers

# Search for the audit documents referenced in line 4
fd -t f 'security-critical-audit-2026-03-08\.md|migration_audit_findings\.md'

# If found, extract finding IDs/numbers from those documents
for file in $(fd -t f 'security.*audit.*\.md|migration.*audit.*\.md'); do
  echo "=== $file ==="
  # Look for common finding ID patterns (e.g., SEC-001, MIG-042, Finding 1.1, etc.)
  rg -n '^\s*(SEC-|MIG-|Finding\s+\d+|##\s*\d+\.)' "$file" | head -30
done

Repository: XOOPS/XoopsCore25

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Find the remediation_proposal.md file
echo "=== Searching for remediation_proposal.md ==="
fd -t f 'remediation_proposal\.md'

# Also search for any audit-related documents
echo ""
echo "=== Searching for all audit-related documents ==="
fd -t f -e md | rg -i 'audit|security|finding|remediation'

# Look for doc directories
echo ""
echo "=== Checking docs directory structure ==="
ls -la docs/ 2>/dev/null || echo "No docs directory"

Repository: XOOPS/XoopsCore25

Length of output: 1622


🏁 Script executed:

#!/bin/bash
# Read the remediation_proposal.md file to check its actual content
cat -n docs/remediation_proposal.md

# Search for the referenced audit documents with broader patterns
echo ""
echo "=== Searching for referenced source documents ==="
fd -t f | rg -i 'security.*2026.*03.*08|migration.*audit|audit.*findings'

Repository: XOOPS/XoopsCore25

Length of output: 6930


Source audit documents are missing from the repository; cannot verify the claimed 22 findings or their mapping to the 15 grouped remediation items.

The document references security-critical-audit-2026-03-08.md and migration_audit_findings.md (line 4) but neither file exists in the repository. Without these source documents, auditors cannot independently verify that the claimed 22 actionable items fixed correspond to actual audit findings, nor trace how those 22 findings map to the 15 grouped remediation items (1A–5A) presented below.

For a security audit remediation record, this traceability is essential. Add one of:

  1. The source audit documents to the repository, or
  2. A crosswalk table mapping the 22 original audit finding IDs to the grouped items below, or
  3. Inline annotations on each grouped row listing which original finding IDs it addresses

Without this, readers cannot confirm audit closure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/remediation_proposal.md` around lines 1 - 14, The remediation record
references missing source audit files (security-critical-audit-2026-03-08.md and
migration_audit_findings.md) so auditors cannot verify the claimed "22
actionable items fixed" or the mapping to grouped items (1A–5A); fix by either
adding the original audit documents to the repo, or adding a crosswalk table in
docs/remediation_proposal.md that maps each original finding ID to the
corresponding grouped remediation item, or by annotating each grouped row
(1A–5A) with the specific original finding IDs it addresses; ensure the chosen
change clearly lists each original finding ID and the remediation commit/PR
references so traceability is restored.

Comment on lines +17 to +56
### Phase 1: Critical (3 items, 10 instances)

| Item | What was fixed | How |
|------|----------------|-----|
| 1A (6 files: `lostpass.php`, `edituser.php`, `register.php`, `profile/changepass.php`, `profile/register.php`, `system/admin/users/main.php`) | Password fields using `getString()` | Changed to `Request::getVar()` with `MASK_ALLOW_RAW \| MASK_NO_TRIM` |
| 1B (2 files: `profile/preloads/core.php`, `profile/lostpass.php`) | Insecure profile lostpass (md5 5-char token) | Disabled preload redirect; `profile/lostpass.php` now redirects to secure core flow |
| 1C (1 file, 3 instances: `system/admin/users/main.php` lines ~85, ~132, ~355) | Missing null guards after `getUser()` in system admin | Added `!is_object()` guards with `_AM_SYSTEM_USERS_NO_SUCH_USER` redirect |

### Phase 2: High (5 items, 11 instances)

| Item | What was fixed | How |
|------|----------------|-----|
| 2A (1 file, 2 instances: `class/xoopscomments.php` lines 268, 444) | Reflected XSS via `$_SERVER['PHP_SELF']` | Replaced with `htmlspecialchars($_SERVER['SCRIPT_NAME'], ENT_QUOTES \| ENT_HTML5, 'UTF-8')` |
| 2B (1 file: `themes/default/modules/system/system_siteclosed.tpl`) | Raw `REQUEST_URI` in site-closed template | Added `\|escape` Smarty modifier |
| 2C (3 files, 6 instances: `include/comment_post.php`, `edituser.php`, `system/admin/users/main.php`) | Content fields (`com_text`, `user_sig`, `bio`) using `getString()` | Changed to `getText()` pinned to `'POST'` |
| 2D (1 file: `system/admin/users/main.php`) | CSRF: `ok` parameter not pinned to POST | Changed to `Request::getInt('ok', 0, 'POST')` |
| 2E (4 files: `include/findusers.php`, `profile/admin/step.php`, `profile/admin/category.php`, `profile/admin/field.php`) | Missing null guards after `get()` | Added `is_object()` + `rank_id > 0` check in findusers; profile admin files fixed in prior commits |

### Phase 3: Security Hardening (3 items, 8 instances)

| Item | What was fixed | How |
|------|----------------|-----|
| 3A (1 file: `class/cache/file.php`) | `eval()` in file cache engine | Removed `eval()` path; changed default `serialize` to `true`; legacy entries treated as cache miss |
| 3B (2 files: `kernel/block.php`, `modules/system/class/block.php`) | `eval()` for custom PHP blocks | Gated behind `XOOPS_ALLOW_PHP_BLOCKS === true` constant check |
| 3C (5 instances: `kernel/object.php` x2, `kernel/configitem.php`, `class/cache/model.php`, `modules/profile/include/update.php`) | Unsafe `unserialize()` without `allowed_classes` | Added `['allowed_classes' => false]`; `configitem.php` uses `set_error_handler()` for corrupted data |

### Phase 4: Compatibility (3 items)

| Item | What was fixed | How |
|------|----------------|-----|
| 4A (2 files: `system/templates/system_imagemanager.tpl`, `system_imagemanager2.tpl`) | Smarty `{php}` tags | Removed `{php}` blocks (fixed in prior commits) |
| 4B (1 directory: `xoops_lib/vendor/smottt/wideimage/demo/`) | PHP 8.2 fatal (`__autoload()`) | Deleted demo directory |
| 4C (1 file: `system/admin/users/main.php`) | Type safety for `$groups_failed` in SQL | Already cast to `(int)` in loop (confirmed correct) |

### Phase 5: Consistency (1 item, 2 files)

| Item | What was fixed | How |
|------|----------------|-----|
| 5A (2 files: `xoops_lib/modules/protector/class/gtickets.php`, `xoops_lib/modules/protector/filters_disabled/postcommon_register_stopforumspam.php`) | Raw superglobals | Migrated to `Xmf\Request::getString()` / `hasVar()` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Standardize instance counts across all phase headers for consistency.

Phases 1-3 use the format "X items, Y instances" (e.g., "3 items, 10 instances"), but Phase 4 omits the instance count entirely ("3 items") and Phase 5 switches to "1 item, 2 files". This inconsistency makes it difficult to trace how the phase-level counts roll up to the "22 actionable items fixed" total.

For auditability and consistency, please standardize all phase headers to use the same format.

📝 Suggested standardization
-### Phase 4: Compatibility (3 items)
+### Phase 4: Compatibility (3 items, 4 instances)

-### Phase 5: Consistency (1 item, 2 files)
+### Phase 5: Consistency (1 item, 2 instances)

If you prefer to count files/directories instead of instances in Phase 4, that's also acceptable—just make the counting methodology explicit (e.g., add a note explaining that Phase 4 counts by file/directory rather than by distinct code fix).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/remediation_proposal.md` around lines 17 - 56, Phase headers are
inconsistent: Phase 4 header ("Phase 4: Compatibility") omits an "instances"
count and Phase 5 uses "1 item, 2 files" instead of "instances"; update the
Phase 4 and Phase 5 headers to match the "X items, Y instances" format used in
Phases 1–3 (e.g., change Phase 4 to "3 items, 3 instances" or to the correct
instance count, and change Phase 5 to "1 item, 2 instances"), and add a short
parenthetical note under Phase 4 if you intentionally count files/directories
instead of instances to document the methodology; modify the markdown headers
and the summary table entries (referencing "Phase 4: Compatibility" and "Phase
5: Consistency") so all phase-level counts follow the same standardized
phrasing.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 85 changed files in this pull request and generated no new comments.

@mambax7 mambax7 merged commit efaed81 into XOOPS:master Mar 8, 2026
13 of 15 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (2f6a36a) to head (80c2008).
⚠️ Report is 10 commits behind head on master.

Additional details and impacted files
@@      Coverage Diff       @@
##   master   #1636   +/-   ##
==============================
==============================

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants