Files
huntingclubroster/MULTI-TENANT-REVIEW-2026-10-20.md

153 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Multi-tenant production review — October 20, 2026
Follow-up: the four confirmed defects below are addressed in
[tenant invitation and ticket repairs](TENANT-REPAIRS-2026-10-20.md). The findings
describe the reviewed version. Administrator MFA, session lifetime and capacity
recommendations are implemented in [the security/capacity follow-up](SECURITY-CAPACITY-2026-10-20.md).
Production acceptance recommendations remain applicable.
The development tenant migration and owner promotion are complete. The earlier
61 development acceptance checks and the broader regression results remain
useful evidence, but they did not cover the edge cases reproduced below.
This review changed no application code or development database records.
Reproductions used generated disposable databases on a separate loopback server.
## Confirmed defects, in recommended repair order
### 1. P1 — Member invitation restores inactive administrator privileges
Location: `classes/ClubInvitations.php:49`; related guard in
`classes/RosterService.php:62`.
The invitation acceptance upsert always activates the membership and preserves
an existing `administrator` role. This conflicts with the rule that only the
Super System Administrator can reactivate/change administrator memberships.
Reproduced:
1. The Super System Administrator deactivates an administrator membership while
retaining its historical administrator role. A second administrator remains.
2. That other club administrator sends a normal **member** invitation to the
inactive administrator's email. No administrator invitation or Super approval
is involved.
3. The recipient accepts. The resulting membership is `active/administrator`,
and `TenantAccess::canManage()` returns true.
This restores management rights within that club; the reproduction did not
grant platform-wide privileges or access to an unrelated club. It is still a
confirmed bypass of the chosen administrator-assignment policy.
Fix: make reactivation/role assignment honor the newly authorized invitation
role. An inactive historical administrator must not regain administrative rights
through a member invitation. Require fresh Super approval for an administrator
grant, and test inactive, demoted, existing active and shared-account cases.
### 2. P2 — Invitation acceptance and membership edits use opposite lock order
Locations: `classes/ClubInvitations.php:42`–`43` and
`classes/RosterService.php:58`–`60`.
Acceptance locks the club and then the user; membership editing locks the user
and then the club. Controlled concurrent execution of the actual service methods
reproduced a MariaDB deadlock (error **1213**): acceptance failed with a
`PDOException` while the membership update succeeded. The services roll back the
failed transaction, but do not retry it; the user receives an application error.
Fix: establish a consistent lock order across invitation, membership and account
flows. Add bounded retry handling for retryable transaction failures, with the
authorization checks rerun inside the retried transaction. Extend concurrency
coverage to acceptance versus editing/revocation, rather than only two accepts.
### 3. P2 — Ticket character limits conflict with the global byte limit
Locations: `classes/Base.php:250`, `classes/ClubTickets.php:4` and `:18`/`:27`.
Tickets advertise/validate an 8,000 Unicode-character message, but the dispatcher
rejects any individual value exceeding 8,192 bytes before ticket validation runs.
A valid 3,000-character message consisting of `界` occupies 9,000 UTF-8 bytes and
was rejected with **HTTP 400 / Invalid request** through the real HTTP handler.
Fix: align field-specific character and byte limits and the HTTP form limits.
Retain request-shape and maximum-size protections; report field-specific errors
instead of rejecting supported text before the field validator can inspect it.
Cover both creation and replies with multibyte boundary tests.
### 4. P2 — Tickets beyond the newest 100 cannot be found through the UI
Locations: `classes/ClubTickets.php:8` and `classes/Roster.php::tickets()`.
The service applies a hard `LIMIT 100`; the page offers no older-page navigation
or search. With 101 owned tickets, the list returned 100 and excluded the oldest.
The older ticket remains in storage and an authorized direct link still works,
but members/administrators cannot discover it through the ticket list.
Fix: add stable server-side paging/search, keeping the club/author access filter
in every page and count query. Include tests for the second page, private-ticket
visibility and cross-club paging attempts.
## Administrator security before public launch
Authentication remains password-only (`classes/AuthService.php:37`), and sensitive
global account/role changes do not require recent authentication. The session
validator has an idle timeout, but no independently enforced absolute lifetime.
The Super System Administrator can access all tenants, so compromise of that
account has platform-wide impact.
I recommend MFA for the Super System Administrator and club administrators,
recent authentication for privilege/email/security changes, a bounded absolute
session lifetime, and a tested recovery process. This is a security-hardening
recommendation, not a newly demonstrated login bypass. OWASP recommends MFA for
[administrative/high-privilege users](https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html#quick-recommendations).
## Shared-service capacity and recovery
Roster members, dues, financial histories, invitation lists and several exports
still load whole collections with `fetchAll()`. Ticket messages are also unpaged.
Current invitation/ticket limits are chiefly per actor, not a total tenant budget.
No realistic large-club/load result was established by this review.
Before general onboarding, establish expected club sizes and concurrent use,
measure expensive pages/exports, add bounded paging/streaming where needed, and
set tenant-level resource limits so one busy club cannot consume shared capacity.
This follows the resource-isolation concerns in
[OWASP's multi-tenant guidance](https://cheatsheetseries.owasp.org/cheatsheets/Multi_Tenant_Security_Cheat_Sheet.html).
The full development backup was restored successfully during migration. A
production recovery plan should additionally explain how to recover one club's
records without rolling back unrelated clubs, and test that procedure.
## Production acceptance work still required
These are deployment checks, not assertions that external hosting configuration
is absent or that the approved development connection needs changing.
- Preserve a reviewed release containing the currently untracked tenant classes,
migration, tests and assets; run CI on that exact release.
- On the actual production host, verify HTTPS/proxy trust, secure session storage,
headers/cookies, secret injection and private-directory denial. The local PHP
router is not evidence that Apache/IIS/Nginx deployment rules are active.
- Run complete tenant/invitation/ticket/financial flows using the actual restricted
runtime database account, with separate migration and backup credentials and
verified TLS for a remote production database.
- Verify real invitation, signup-verification and reset inbox delivery, trusted
links, sender-domain authentication and delivery-failure monitoring. Recent
development acceptance deliberately sent no external messages.
- Install/verify backup, retention, cleanup and health schedules, off-host
protected recovery copies, alert routing and log rotation. Exercise failure
alerts for SMTP, mandatory audit writes, backup jobs and storage exhaustion.
- Explicitly assign the intended club administrators before onboarding members.
Development currently has one existing club and no real memberships; the
security tests correctly used owned temporary assignments.
## Review evidence and limits
- Confirmed the privilege restoration, opposite-lock-order deadlock, Unicode HTTP
rejection and 101-ticket list truncation in disposable local databases.
- Current `composer audit --locked` returned no advisories or abandoned packages.
This does not cover all vendored browser assets or establish that hosting is safe.
- Did not rerun the full regression suite, load-test production, send real email,
or modify development records. The new edge cases need permanent regression
coverage with their fixes.
- Removed the disposable databases and stopped their separate loopback server
after the review.