generated from jric11/baseProject
119 lines
6.6 KiB
Markdown
119 lines
6.6 KiB
Markdown
|
|
# Multi-tenant production readiness review — October 20, 2026
|
|||
|
|
|
|||
|
|
Follow-up: the two MFA defects below have been repaired. See
|
|||
|
|
[MFA lifecycle repairs](MFA-REPAIRS-2026-10-20.md) for the implementation and
|
|||
|
|
regression coverage. The findings below preserve the original review evidence.
|
|||
|
|
|
|||
|
|
Reviewed the current authentication/MFA lifecycle, tenant permissions and routes,
|
|||
|
|
invitations, roster/ledger paging and exports, shared budgets, deployment tooling
|
|||
|
|
and dependency state. No application code, server configuration or development
|
|||
|
|
database records were changed by this review. Reproductions used disposable
|
|||
|
|
local databases and generated accounts, with external email disabled.
|
|||
|
|
|
|||
|
|
## Confirmed defects, recommended repair order
|
|||
|
|
|
|||
|
|
### 1. P1 — MFA replacement temporarily permits password-only factor takeover
|
|||
|
|
|
|||
|
|
Location: `classes/MfaService.php:182`–`200`, particularly the deletion at
|
|||
|
|
lines 188–191; enrollment checks at lines 107–141.
|
|||
|
|
|
|||
|
|
After verifying the current password and an existing authenticator/recovery code,
|
|||
|
|
replacement immediately deletes the current factor and all recovery codes. The
|
|||
|
|
replacement authenticator has not yet been configured or proved. The account
|
|||
|
|
therefore returns to the initial-enrollment path for any new password login.
|
|||
|
|
|
|||
|
|
Reproduced through the real HTTP handlers with two independent cookie jars:
|
|||
|
|
|
|||
|
|
1. A temporary Super System Administrator enrolls MFA normally.
|
|||
|
|
2. Its legitimate session starts authenticator replacement with valid credentials.
|
|||
|
|
3. Before replacement is confirmed, a separate session supplies only the account
|
|||
|
|
password. It has neither the old authenticator nor an unused recovery code.
|
|||
|
|
4. That session receives a new setup secret, enrolls its own authenticator and
|
|||
|
|
successfully opens the Super System Administrator account-management page.
|
|||
|
|
|
|||
|
|
This requires knowledge of the password and overlap with an authorized
|
|||
|
|
replacement window. It does not bypass a stable enrolled factor outside that
|
|||
|
|
window. Its impact is nevertheless platform-wide for the Super account.
|
|||
|
|
|
|||
|
|
Fix: retain the existing factor while a replacement is pending. Bind replacement
|
|||
|
|
authorization and the new secret to the authorized session and account version,
|
|||
|
|
then atomically swap the factor, regenerate recovery codes and revoke other
|
|||
|
|
sessions only after confirmation of the new authenticator. New password-only
|
|||
|
|
sessions must continue to face the existing MFA challenge. Abandonment, expiry,
|
|||
|
|
concurrent replacements and password resets must not reopen initial enrollment.
|
|||
|
|
|
|||
|
|
### 2. P2 — Wrong password consumes a valid recovery code or authenticator step
|
|||
|
|
|
|||
|
|
Location: `classes/MfaService.php:177`–`181`; consumption in `verify()` at
|
|||
|
|
lines 80–105.
|
|||
|
|
|
|||
|
|
The page computes `$passwordOk`, but invokes the mutating factor verifier before
|
|||
|
|
testing that boolean. The verifier commits recovery-code deletion/counter
|
|||
|
|
advancement and its audit before the page rejects the incorrect password.
|
|||
|
|
|
|||
|
|
Reproduced: an account has ten recovery codes; submitting one with an incorrect
|
|||
|
|
password returns HTTP 403 and leaves nine codes. Submitting that same recovery
|
|||
|
|
code with the correct password also returns HTTP 403. The valid recovery code
|
|||
|
|
has been lost despite the failed authentication.
|
|||
|
|
|
|||
|
|
Fix: validate both credentials before committing factor consumption. Preserve
|
|||
|
|
rate limiting for incorrect passwords as well as incorrect codes, and keep
|
|||
|
|
consumption/auditing atomic. Verify rollback and concurrent use explicitly.
|
|||
|
|
|
|||
|
|
## Security recommendation before public launch
|
|||
|
|
|
|||
|
|
MFA enrollment, recovery use and replacement write audit records, but do not
|
|||
|
|
notify the account owner through an independent channel. Add security notices
|
|||
|
|
for factor changes/recovery, with visible or monitored delivery failures and
|
|||
|
|
without putting secrets or recovery codes in email. This is a recommendation,
|
|||
|
|
not another reproduced authorization bypass. It follows
|
|||
|
|
[OWASP's factor-change notification guidance](https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html).
|
|||
|
|
|
|||
|
|
## Production acceptance and operations still required
|
|||
|
|
|
|||
|
|
The previous development fixes are present: administrator MFA/QR setup, absolute
|
|||
|
|
and idle timeouts, bounded lists/exports, and shared club budgets. The recent
|
|||
|
|
server follow-up confirmed Apache key access and private-path denial on
|
|||
|
|
ositsdevels. Development HTTP and its approved database connection are not new
|
|||
|
|
production defects.
|
|||
|
|
|
|||
|
|
Before public onboarding:
|
|||
|
|
|
|||
|
|
- Preserve all required new/untracked classes, migrations, tests, assets and
|
|||
|
|
dependency changes in a reviewed, versioned release; run CI on that exact
|
|||
|
|
release. A current workspace build does not prove a clean checkout is complete.
|
|||
|
|
- Run acceptance on the actual production HTTPS host: proxy trust, cookies and
|
|||
|
|
headers, private paths, session/log permissions, and injected production secrets.
|
|||
|
|
The development key-file override is deliberately unavailable in Production;
|
|||
|
|
the production MFA key and a protected recovery copy must be provisioned. If
|
|||
|
|
migrating existing enrolled accounts, preserve their key or perform an explicit
|
|||
|
|
factor migration rather than silently changing it.
|
|||
|
|
- Exercise complete tenant, MFA, invitation, reset and financial workflows with
|
|||
|
|
the actual restricted production database account. Verify remote production
|
|||
|
|
database TLS and separate migration/backup credentials.
|
|||
|
|
- Verify real invitation, verification and reset inbox delivery, trusted links,
|
|||
|
|
sender authentication and monitored delivery failures.
|
|||
|
|
- Verify scheduled backups, cleanup and health checks, alert routing, log
|
|||
|
|
rotation and storage monitoring. Test off-host restore of both SQL and MFA
|
|||
|
|
key material. Document and exercise recovery of a single club without rolling
|
|||
|
|
back unrelated clubs.
|
|||
|
|
- Establish expected club sizes/concurrency and measure sustained workloads.
|
|||
|
|
Bounded rows and request budgets improve resource control but do not establish
|
|||
|
|
production throughput or response-time targets.
|
|||
|
|
|
|||
|
|
These are requirements to verify on the selected production infrastructure,
|
|||
|
|
not claims that externally configured jobs or controls are absent.
|
|||
|
|
|
|||
|
|
## Evidence and limits
|
|||
|
|
|
|||
|
|
- Both MFA findings reproduced against actual service/HTTP behavior using
|
|||
|
|
independent sessions and disposable data.
|
|||
|
|
- No additional cross-club authorization bypass was demonstrated in this review;
|
|||
|
|
this does not replace a full penetration test or independent security review.
|
|||
|
|
- `composer audit --locked` reported no security vulnerability advisories.
|
|||
|
|
- Did not rerun the entire regression suite, load-test production, send real
|
|||
|
|
email, or modify real accounts. Existing happy-path replacement tests do not
|
|||
|
|
cover the competing password-only session reproduced here.
|
|||
|
|
- Private reproduction helper: `.tools/review-mfa-replacement.py`.
|
|||
|
|
- Removed the disposable databases and stopped their loopback server after review.
|