# Variant Root Cause Analysis — CVE-2026-48611 (phpBB login-link auth bypass)

## Summary

No bypass or materially-distinct alternate trigger of CVE-2026-48611 was found
on the fixed phpBB 3.3.17. The CVE's root cause is two cooperating defects:
(1) an attacker-steerable auth-provider selection
(`provider_collection::get_provider($request->variable('auth_provider', ''))`)
and (2) the password-less `apache` provider `login()` that returns
`LOGIN_SUCCESS` for any existing username carried in the HTTP Basic
`Authorization` header without validating the password, after which the caller
invokes `$user->session_create(user_id)` → unauthenticated account hijack.
The 3.3.17 fix deleted `includes/ucp/ucp_login_link.php`, redirected
`ucp.php?mode=login_link` to a new `phpbb/ucp/controller/oauth.php`
controller, and changed provider resolution in that flow to `get_provider()`
with **no argument** (→ board-configured `auth_method`, default `db`), so the
`apache` password-less sink is no longer reachable from request-steered
provider selection. A systematic variant matrix (4 materially-distinct
entry/data paths + the original-exploit control) was executed against both the
vulnerable 3.3.16 and the fixed 3.3.17 builds: the fixed build blocks every
candidate (`*_u=1`, anonymous, no admin session), while the 3.3.16 control
still hijacks `admin` (`user_id=2`) with the ACP link present. One residual
instance of the *anti-pattern* (attacker-steerable `get_provider()` in
`includes/ucp/ucp_register.php`) survives the fix, but it does **not** reach
the password-less `login()` sink and produces no account hijack on either
version, so it is a defense-in-depth gap, not a bypass.

## Fix Coverage / Assumptions

- **Invariant the fix relies on:** "Authentication providers must never be
  selectable from untrusted request input; provider resolution should always
  use the board-configured `auth_method`." The new `oauth` controller enforces
  this by calling `$this->auth_collection->get_provider()` with no argument
  in `link_account()`, `authenticate()`, and `login()`.
- **Code paths explicitly covered:** `ucp.php?mode=login_link` (now a
  redirect), the new controller routes `/oauth/link_account`,
  `/oauth/authenticate/{svc}`, `/oauth/login/{svc}` (the latter additionally
  guarded by an `instanceof \phpbb\auth\provider\oauth\oauth` check that
  throws HTTP 401 for non-oauth-configured boards).
- **What the fix does NOT cover:** `includes/ucp/ucp_register.php` still
  resolves the provider with
  `get_provider($request->variable('auth_provider', ''))` when `login_link_*`
  POST data is present. This is the same request-steerable pattern that was
  the root cause, and it was left untouched. Decisively, though, the register
  flow uses that provider **only** for `login_link_has_necessary_data()` and
  `link_account()` — it never calls `->login()` on it, and its only
  `session_create()` is for the *newly registered* user. So the dangerous
  sink is not reachable from this residual instance.

## Variant / Alternate Trigger

Four materially-distinct candidate entry/data paths were tested against the
fixed 3.3.17 (each also run against 3.3.16 for parity/control):

- **V1 — Residual attacker-controlled provider in the REGISTER flow.**
  Entry point: `POST ucp.php?mode=register&auth_provider=apache` with
  `login_link_aikido=1` in the body + registration fields (and, on the vuln
  build, an `Authorization: Basic admin:x` header).
  Code path: `includes/ucp/ucp_register.php::main()` (~line 120)
  `get_provider($request->variable('auth_provider', ''))` →
  `apache::login_link_has_necessary_data()` (inherits
  `base::login_link_has_necessary_data()` → returns `'LOGIN_LINK_MISSING_DATA'`)
  and `apache::link_account()` (inherits `base::link_account()` → no-op).
  Result: no admin session on either version (`*_u=1`). The register flow
  never calls `->login()` on the selected provider, so the password-less sink
  is unreachable. **Not a variant of the auth-bypass.**

- **V2 — `auth_provider` forwarded through the login_link redirect.**
  Entry point: `POST ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1`.
  `ucp.php` forwards *all* GET params (incl. `auth_provider`) to
  `/app.php/user/oauth/link_account` via `phpbb_redirect_to_controller()`.
  Code path: the controller `link_account()` calls `get_provider()` with **no
  argument** and never reads `auth_provider`.
  Result on fixed: `301 Moved Permanently` → controller → `*_u=1`. The
  forwarded `auth_provider` is ignored. **Blocked.**

- **V3 — Direct controller hit with `auth_provider=apache` in the query
  string.**
  Entry point:
  `POST /app.php/user/oauth/link_account?auth_provider=apache&login_link_aikido=1`
  + `Authorization: Basic admin:x` + `login_username=admin&login_password=x`.
  Code path: `oauth::link_account()` → `get_provider()` no-arg → `db` →
  `db::login('admin','x')` verifies the password hash → rejects.
  Result on fixed: `200` login-link form with a `class="error"` block,
  `*_u=1`. `auth_provider` in the query is never read by the controller.
  **Blocked.**

- **V4 — The new oauth LOGIN controller with a Basic header.**
  Entry point: `GET /app.php/user/oauth/login/apache` +
  `Authorization: Basic admin:x`.
  Code path: `oauth::login('apache')` → `get_provider()` no-arg → `db` →
  `if (!$provider instanceof \phpbb\auth\provider\oauth\oauth) throw 401`.
  Result on fixed: `401 Unauthorized`, `*_u=1`. The `instanceof` guard
  rejects the default `db`-configured board. **Blocked.**

- **V5 — Original exploit (CONTROL).**
  Entry point:
  `POST ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1` +
  `Authorization: Basic admin:x`.
  Result on 3.3.16: `302`, `Set-Cookie *_u=2`, ACP link present → **admin
  hijack confirmed** (parity with the repro). Result on 3.3.17 (ucp.php path):
  `301` → controller → `*_u=1`; (controller path): `200` form + error,
  `*_u=1`. **Fixed build blocks the original exploit.**

A whole-repo search in 3.3.17 for
`get_provider($request->variable('auth_provider'` returns exactly one hit
(`ucp_register.php:120`, analyzed as V1). Every other `get_provider(` call
site (in `auth.php`, `session.php`, `functions.php`, `ucp_auth_link.php`, and
all three `oauth.php` controller actions) passes no argument. There is no
remaining request-steerable `get_provider()` that is followed by a
`->login()` + `session_create()` chain.

## Impact

- **Package/component affected (by the residual anti-pattern):**
  `phpBB/includes/ucp/ucp_register.php` (attacker-steerable provider
  selection) — but the dangerous sink (`apache::login()` → `session_create()`
  for an arbitrary existing user) is **not** reachable from it.
- **Affected versions as tested:** phpBB `release-3.3.16`
  (commit `555f0aaa6b892efb2e6b6edd2362302a3ef8b339`) and `release-3.3.17`
  (commit `3508484fdc18cd97eeab229da830055c79fcc59e`).
- **Risk level and consequences of the residual gap:** Low / defense-in-depth
  only. No account hijack is achievable through it today. It is a latent
  inconsistency with the fix's stated invariant that should be closed to
  prevent a future regression from turning it into a real bypass.

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE):** Unauthenticated remote
  account hijacking of arbitrary known accounts (including administrators) →
  full board compromise; no password, no prior access, no user interaction.
- **Reproduced impact from this variant run:** None on the fixed build. The
  fixed 3.3.17 blocked every candidate (V1–V4) with `*_u=1` (anonymous) and
  no admin session. The 3.3.16 control (V5) reproduced the parent impact
  (`_u=2` + ACP link) confirming the test harness is sound and the original
  exploit still works on the vulnerable version.
- **Parity:** `none` (no bypass / no alternate trigger reproducing the CVE
  impact on the fixed build).
- **Not demonstrated:** No admin-session creation via any path other than the
  original `ucp.php?mode=login_link&auth_provider=apache` exploit on the
  vulnerable 3.3.16.

## Root Cause

The CVE's exploitable chain requires **both** (1) a request-steerable
`get_provider($request->variable('auth_provider', ''))` and (2) a subsequent
`->login()` on the selected provider whose result feeds
`session_create()` for an existing user. The 3.3.17 fix eliminates (1) from
the login-link flow by switching to `get_provider()` with no argument, so on a
default `db` board the only provider reachable is `db`, whose `login()`
verifies the password hash and rejects the wrong password. The single
surviving request-steerable `get_provider()` (in `ucp_register.php`) is not
followed by a `->login()` call — it is followed only by
`login_link_has_necessary_data()` (which the `apache` provider inherits as a
hard-coded `'LOGIN_LINK_MISSING_DATA'` error) and `link_account()` (a no-op
for `apache`) — so defect (2) is not reachable from it. Therefore the same
underlying bug **cannot** still be reached on the fixed build via any of the
materially-distinct entry/data paths examined.

Fix commits (in `release-3.3.16..release-3.3.17`): `12c4cf6f78`,
`c05603cc3a`, `4a2962bfc8` (`[ticket/17659]` series).

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` (self-contained,
   idempotent; run with
   `PRUVA_ROOT=<bundle dir> bash bundle/vuln_variant/reproduction_steps.sh`).
   It reuses the Docker images (`phpbb-cve2026-48611:vuln` / `:fixed`) built
   by the repro stage.
2. **What it does:** starts both containers, then issues real HTTP requests
   from inside each container against the running Apache+mod_php service:
   - V5 control: original exploit on 3.3.16 (expect admin hijack) and on 3.3.17
     via both `ucp.php` and the controller (expect blocked).
   - V1: register flow with `auth_provider=apache` + `login_link_*` on both
     builds (expect no admin session on either).
   - V2: `auth_provider` forwarded via the `ucp.php?mode=login_link`
     redirect on 3.3.17 (expect `301` → `*_u=1`).
   - V3: direct controller `/oauth/link_account?auth_provider=apache` on
     3.3.17 (expect form + error, `*_u=1`).
   - V4: oauth login controller `/oauth/login/apache` with a Basic header on
     3.3.17 (expect `401`, `*_u=1`).
   - Verdict: exit `0` if any fixed-build path mints an admin session
     (`_u=2`); exit `1` if every candidate is blocked (the actual outcome).
3. **Expected evidence:** `bundle/logs/vuln_variant.log` shows
   `vuln_hijack=yes; fixed_original_blocked=yes; fixed_variant_hit=no` and
   `RESULT: NO BYPASS`. Per-candidate response captures live under
   `bundle/logs/vv_*.txt` (e.g. `vv_fixed_v3_resp.txt` shows the controller
   rendered the login-link form with `Set-Cookie *_u=1` and a
   `class="error"` block; `vv_fixed_v4_resp.txt` shows `401 Unauthorized`;
   `vv_vuln_v5_resp.txt` shows the `302` + `Set-Cookie *_u=2` hijack on
   3.3.16).

## Evidence

- `bundle/logs/vuln_variant.log` — annotated run log (two consecutive runs,
  identical verdicts).
- `bundle/logs/vv_vuln_v5_resp.txt` — 3.3.16 original exploit: `302 Found` +
  `Set-Cookie: phpbb3_..._u=2` (admin) → hijack confirmed (control/parity).
- `bundle/logs/vv_fixed_v5ctrl_resp.txt` — 3.3.17 controller path: `200` form
  with `class="error"` + `Set-Cookie: phpbb3_..._u=1` (anonymous) → original
  exploit blocked.
- `bundle/logs/vv_fixed_v1_register_resp.txt` — 3.3.17 register flow with
  `auth_provider=apache`: agreement/form page, `*_u=1`, no admin session.
- `bundle/logs/vv_fixed_v3_resp.txt` — 3.3.17 controller with
  `auth_provider=apache` in query: `200` form, `Set-Cookie *_u=1`,
  `class="error"` block (controller ignores `auth_provider`).
- `bundle/logs/vv_fixed_v4_resp.txt` — 3.3.17 oauth login controller:
  `401 Unauthorized`, `*_u=1` (`instanceof oauth\oauth` guard).
- `bundle/vuln_variant/runtime_manifest.json` — runtime evidence manifest
  (`entrypoint_kind=api_remote`, `service_started=true`,
  `healthcheck_passed=true`, `target_path_reached=true`,
  `bypass_found=false`).
- `bundle/vuln_variant/artifacts/` — raw per-candidate cookie jars + response
  captures for both builds.
- **Environment:** Docker `php:8.2-apache` (Apache/2.4.67, PHP/8.2.32,
  mod_php), phpBB `release-3.3.16` (commit `555f0aaa...`) and
  `release-3.3.17` (commit `3508484f...`), SQLite3 backend, default
  `auth_method=db`, board installed via `php install/phpbbcli.php install`.
  Exact tested revisions resolved via `git rev-parse <tag>^{commit}` and
  recorded in `bundle/vuln_variant/source_identity.json`.

## Recommendations / Next Steps

- **No emergency action beyond the 3.3.17 upgrade is needed** for the
  auth-bypass itself — the fix closes the account-hijack path on default
  `db` boards.
- **Close the residual anti-pattern for defense-in-depth / future-proofing:**
  change `includes/ucp/ucp_register.php` (~line 120) to call
  `$provider_collection->get_provider();` (no argument) instead of
  `get_provider($request->variable('auth_provider', ''))`, so provider
  selection in the register flow also uses the board-configured
  `auth_method`. This aligns the register flow with the fix's stated
  invariant and removes the latent risk that a future refactor adds a
  `->login()` call on the request-steered provider.
- **Add a regression test** asserting that no unauthenticated UCP entry point
  (`login_link`, `register`, the oauth controllers) ever honors an
  attacker-supplied `auth_provider`, and that the `apache` provider cannot
  return `LOGIN_SUCCESS` without external-auth configuration.
- **Optional hardening:** gate the `apache` provider's trust model
  (`PHP_AUTH_USER` is trusted) to boards that actually configure Apache/LDAP
  front-auth, and refuse to honour it through flows designed for OAuth/db
  logins.

## Additional Notes

- **Idempotency:** `reproduction_steps.sh` was executed twice consecutively;
  both runs exited `1` (no bypass) with identical verdicts
  (`vuln_hijack=yes; fixed_original_blocked=yes; fixed_variant_hit=no`).
  Re-runs reuse the cached Docker images and only re-start containers and
  re-issue HTTP requests, completing in a few seconds.
- **Bounded search justification:** the variant search was bounded by a
  whole-repo scan for the *combination* that defines the root cause — a
  request-steerable `get_provider($request->variable('auth_provider', ''))`
  that is followed by a `->login()` + `session_create()` chain. In 3.3.17
  that combination exists in zero places: the only request-steerable
  `get_provider()` left (`ucp_register.php`) is not followed by `->login()`.
  The other request-facing `get_provider()` call sites all pass no argument.
  Additional "attempts" beyond V1–V4 would be the same root cause/surface
  under a different name (e.g. calling `link_account` vs `authenticate` on
  the same controller that already uses no-arg `get_provider()`), so they
  are not materially distinct candidates.
- **Negative control integrity:** the 3.3.16 build still hijacks admin via
  the original exploit (`_u=2` + ACP link), proving the test harness reaches
  the real Apache+mod_php boundary and that the "blocked" results on 3.3.17
  are due to the fix, not a broken exploit.
- **Sandbox networking:** host→container port publishing is blocked, so
  exploit traffic is issued from inside each container via
  `docker exec curl http://127.0.0.1:80/...`, which still crosses the genuine
  Apache + mod_php request boundary (mod_php populates
  `PHP_AUTH_USER`/`PHP_AUTH_PW` from the `Authorization: Basic` header) and is
  equivalent to an external attacker hitting the deployed forum over HTTP.
