Skip to content

Security and correctness fixes from a Codex-assisted review - #36

Merged
bonelifer merged 9 commits into
masterfrom
codex-review-fixes
Sep 22, 2026
Merged

bonelifer merged 9 commits into
masterfrom
codex-review-fixes

Conversation

@bonelifer

Copy link
Copy Markdown
Contributor

Summary

Nine focused fixes addressing 14 findings from a Codex-assisted security review of this extension (auth/CSRF, permission boundaries, restriction lifecycle, and several data-hygiene bugs). Each commit is scoped to one concern; see individual commit messages for the full reasoning and the exact phpBB core behavior each fix was verified against.

  • fix(security): stop cancel=1 from bypassing ban/restrict confirmation — a forged cancel=1 POST let both the ban and restrict actions execute without ever passing through a real confirmation, exploitable via CSRF against an authenticated moderator.
  • fix(security): validate ACP move/restrict group selections — a non-founder admin could configure a founder-managed group as the move/restrict target and use it to grant another account elevated permissions.
  • fix(security): require explicit permission for cross-forum post deletion — del_posts let any moderator with m_ban delete a user's posts in forums they have no delete permission in. Added a new, independently-grantable permission (m_banhammer_del_posts_all) for that broader authority; a migration copies it onto everyone who currently has m_ban, so existing behavior is unchanged today, but it's an explicit, auditable grant going forward.
  • fix: load functions_user.php before user_ban() in the domain-ban controller — confirming a domain ban could hit an undefined-function error and silently create no ban.
  • fix: correct restriction lifecycle bugs around group changes — expiry not restoring the original default group, a changed ACP restrict-group setting stranding active restrictions, the ban/restrict group cleanup fighting an active restriction when both settings match the same group, and purging the extension leaving restricted users stuck with no way back.
  • fix: use phpBB's own cleanup for a banned user's PMs and poll votes — hand-rolled deletes were leaving recipients' unread counts, notifications, and attachments inconsistent, and corrupting poll totals.
  • fix: MCP quick-ban link skips ACP-configured defaults — the post-approval quick-ban shortcut silently ignored configured ban defaults (e.g. a 7-day default becoming a permanent ban).
  • fix: check curl_exec() result for SFS reports, not just HTTP code — a transport failure after headers were sent could be reported to the moderator as a successful report.
  • fix: malformed BH_STYLE attribute and dead ACP success-message markup — cosmetic cleanup.

Test plan

  • php -l on every changed/added PHP file
  • Enabled the extension against a real phpBB 3.3.x + SQLite3 + PHP 8.4 install, confirming every migration (including the two new ones) applies cleanly
  • Manually seeded an active restriction and ran extension:purge, confirming the user is correctly restored to their original default group and removed from the restrict group before the tracking table is dropped
  • Re-enabled cleanly after purge; verified the new m_banhammer_del_posts_all permission is created and copied onto ROLE_MOD_FULL (which carries m_ban), and cleanly removed on purge

Investigated and written by Claude on behalf of William Jacoby (bonelifer).

bonelifer and others added 9 commits September 22, 2026 16:41
A forged cancel=1 POST (no confirm_key) made confirm_box(true) fail, then
confirm_box(false, ...) return false without rendering anything instead
of exiting, letting execution fall straight through to the ban or
restriction code below - CSRF-exploitable against an authenticated
moderator's session. Both call sites now return immediately after
confirm_box(false, ...), matching phpBB core's own confirm_box() idiom
(e.g. includes/mcp/mcp_ban.php), where the gated action only ever runs
inside a successful confirm_box(true) branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
set_options() stored the submitted move_group/restrict_group id with no
validation, and the dropdown only hid special/founder-managed groups
client-side. A non-founder admin with a_user and m_ban could configure a
founder-managed privileged group as the move/restrict target, then grant
another account its permissions by moving them into it - phpBB's own
acp_users.php explicitly rejects this same operation for non-founders.

set_options() now rejects a submitted group that's a special group or
founder-managed (unless the current user is the founder), and get_groups()
also hides founder-managed groups from non-founders in the dropdown,
matching acp_users.php's own display logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bh_del_posts() deleted every post a banned user ever made across every
forum, gated only on m_ban. A moderator with m_ban but no delete
permission in a particular forum could still wipe the target's posts
there, and setting the ACP deletion default to "No" didn't prevent it
either since del_posts=1 could still be submitted directly.

Adds a new, independently-grantable permission,
m_banhammer_del_posts_all, for the cross-forum authority; without it,
bh_del_posts() now only deletes posts in forums the acting moderator
already has m_delete in. The new migration copies the permission onto
anyone who currently holds m_ban (via ROLE_MOD_FULL and any direct
grants), so existing installs keep today's behavior unchanged, but going
forward it's an explicit, auditable grant rather than a hidden side
effect of m_ban.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…roller

ban_domain_controller is routed through app.php, which doesn't load
includes/functions_user.php by default (unlike common.php's request
flow, which the profile-page ban listener already accounts for). Without
another extension incidentally loading it first, confirming a domain ban
hit an undefined-function error on user_ban() and created no ban.

root_path/php_ext are now injected the same way the listener and cron
task already get them, and the controller includes functions_user.php on
demand before calling user_ban(), matching the guard used everywhere else
in this extension.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four related bugs in the "restrict instead of ban" feature:

- Expiry didn't restore the original default group: group_user_add()
  sees the user as already a member (they were never removed from their
  original group while restricted) and returns GROUP_USERS_EXIST before
  reaching the code that sets the default group. Now calls
  group_user_attributes('default', ...) directly, which is what
  group_user_add() would have called internally.

- Changing the ACP restrict group while a restriction was still active
  stranded the user: the cron task read the *current* config value for
  every expiring row instead of what was actually applied. A new
  restrict_group_id column records the group used at restriction time,
  written by do_restrict_stuff() and read by the cron task per row, with
  a migration that backfills existing rows from the current setting as
  the best available guess.

- Configuring the same group for both bans and restrictions caused the
  ban-expiry group cleanup (undo_bh_group) to immediately undo an active
  restriction on its next session check, since it can't tell a
  restricted-not-banned user from a leftover ban-group member. It now
  checks active_restriction() first when the two settings match.

- Purging the extension dropped the restriction-tracking table without
  restoring anyone still actively restricted, stranding them in the
  restrict group permanently with no way back. The new migration's
  revert_data() now restores every still-tracked user (group and
  default) before its own revert_schema() drops the column, and before
  restrict_group.php's own revert drops the table next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bh_del_privmsgs() hand-deleted PM rows without updating recipients'
unread/new counts, custom-folder counts, or attachments, and could leave
recipients with unread notifications pointing at a message that no
longer exists. It now delegates to phpbb_delete_users_pms(), the same
bulk cleanup phpBB's own account deletion uses, and only handles the
banned user's own folder/rule rows directly (their own account settings,
which don't affect anyone else).

bh_del_posts() also deleted every poll vote the user ever cast, in any
topic, without decrementing poll_option_total - corrupting poll totals
and letting the user vote again after a temporary ban expired, since
their prior-vote record was gone. phpBB's own user_delete() doesn't
touch POLL_VOTES_TABLE at all for the same reason; this now matches that
and leaves votes alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The MCP post-approval "Ban Hammer" link went straight to
memberlist.php?...&bh=1 with no other parameters, which skips the
profile page's options form entirely and lands directly on the
confirmation step with every option defaulted to zero: permanent
duration, no email/IP ban, no deletions, no group move, no SFS report.
An admin's configured seven-day-ban default, for example, silently
became a permanent username-only ban through this shortcut.

The link now points at the plain profile page instead, which shows the
real options form reflecting the ACP-configured defaults, same as
visiting the profile directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_file() discarded curl_exec()'s return value and only inspected the
HTTP status code. If the server sent 200 headers and the transfer then
timed out, curl_exec() returns false but the recorded HTTP code still
reads 200, so the moderator was told "All actions were performed
correctly" despite the Stop Forum Spam report never actually completing.
Now treats a false return from curl_exec() as a failure too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BH_STYLE ended with a stray embedded double quote even though the
template already supplies its own attribute quotes, producing malformed
HTML like style="background-color: green; color: white;";" on both
success and failure results. The value is fixed text either way, not a
templating bug, so this isn't an XSS finding - just broken markup.

Also removed the ACP template's S_SAVED success box: nothing ever
assigns S_SAVED, since a successful save goes through trigger_error()
like every other success message in this extension, so the block was
unreachable dead code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bonelifer
bonelifer merged commit fb99cd8 into master Sep 22, 2026
5 checks passed
@bonelifer
bonelifer deleted the codex-review-fixes branch September 22, 2026 21:48
@bonelifer

Copy link
Copy Markdown
Contributor Author

@kaileymsnay A little late, but could you still look over this?

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.

1 participant