# Verification Report — CVE-2026-48611 (phpBB login-link auth bypass)

## Fix Summary

CVE-2026-48611 is an unauthenticated authentication bypass in phpBB 3.3.0–3.3.16
where the UCP "login-link" flow (`ucp.php?mode=login_link`) resolved the
authentication provider from a fully attacker-controlled `auth_provider` request
parameter. By forcing the bundled, password-less `apache` provider and sending an
HTTP Basic `Authorization` header carrying any existing username (e.g. `admin`),
an attacker obtained a valid session as that user — including founders/admins —
without knowing the password, because `apache::login()` trusts `PHP_AUTH_USER`
and returns `LOGIN_SUCCESS` after a database lookup without ever comparing the
password.

The applied fix removes the attacker-controlled provider selection from the
login-link flow (and, for defense-in-depth, from the registration flow's
login-link branch). Both call sites now resolve the provider via
`provider_collection->get_provider()` with **no argument**, which selects the
board-configured `auth_method` (default `db`). The `db` provider's `login()`
actually verifies the password hash, so the wrong password is rejected and no
session is created. This is the exact security-relevant change the official
phpBB 3.3.17 fix makes (it additionally moved the flow into a new
`phpbb/ucp/controller/oauth.php` controller, but the central fix — dropping the
attacker-supplied `auth_provider` argument — is identical and is what closes the
account-hijack path). Legitimate OAuth account linking is preserved: on a board
configured for OAuth, `auth_method=oauth`, so `get_provider()` still returns the
`oauth` provider (the OAuth provider delegates username/password login to the
`db` provider internally, so no behavior changes for OAuth boards).

## Changes Made

The patch (`bundle/coding/proposed_fix.diff`) modifies two files with minimal,
surgical edits. Both changes replace a request-steerable
`get_provider($request->variable('auth_provider', ''))` call with the
no-argument `get_provider()` that uses the board-configured `auth_method`.

1. **`phpBB/includes/ucp/ucp_login_link.php`** (the CVE's primary entry point)
   - Replaced `$auth_provider = $provider_collection->get_provider($request->variable('auth_provider', ''));`
     with `$auth_provider = $provider_collection->get_provider();`
   - Updated the preceding comment (which previously documented the vulnerable
     "Use the auth_provider requested even if different from configured"
     behavior) to document the security invariant: provider resolution must
     always use the board-configured `auth_method` and never the request
     parameter (CVE-2026-48611).
   - This is the change that closes the exploit: the `apache` password-less
     provider is no longer reachable from `ucp.php?mode=login_link`.

2. **`phpBB/includes/ucp/ucp_register.php`** (~line 120, defense-in-depth)
   - In the `if (!empty($login_link_data))` branch of `ucp_register::main()`,
     replaced the identical request-steerable `get_provider(...)` call with the
     no-argument `get_provider()`, plus an explanatory comment.
   - The variant analysis confirmed this residual instance of the anti-pattern
     does **not** reach the dangerous `->login()` + `session_create()` sink today
     (the register flow only calls `login_link_has_necessary_data()` and
     `link_account()` on the selected provider, and its only `session_create()`
     is for the newly registered user), so it is not itself a bypass. It is
     closed here for consistency with the fix's stated invariant and to remove
     the latent risk that a future refactor adds a `->login()` call on the
     request-steered provider. This change is behavior-preserving: on an
     OAuth-configured board `auth_method=oauth`, so `get_provider()` returns the
     same `oauth` provider as the previous `get_provider('oauth')`; on a default
     `db` board the register flow with `login_link_*` data is not legitimately
     reachable (it only originates from an OAuth redirect, which requires
     `auth_method=oauth`).

Both edits are one-line logical changes that match the official 3.3.17 fix's
security invariant ("authentication providers must never be selectable from
untrusted request input; provider resolution should always use the
board-configured `auth_method`"). No other files, routes, services, or templates
are touched.

## Verification Steps

The verification script (`bundle/coding/verify_fix.sh`) builds a Docker image
from the **vulnerable** phpBB `release-3.3.16` source with the proposed patch
applied (`phpbb-cve2026-48611:patched`), runs the real Apache 2.4 + mod_php 8.2
service, and issues the actual exploit (and a legitimate login) from inside the
container against `127.0.0.1:80`, crossing the genuine request boundary (so
`PHP_AUTH_USER`/`PHP_AUTH_PW` are populated by mod_php from the `Authorization:
Basic` header, exactly as in the repro). Host→container port publishing is
blocked in this sandbox, so traffic is issued via `docker exec ... curl`, which
is equivalent to an external attacker hitting the deployed forum.

1. **Patched source preparation.** Copied the cached vulnerable 3.3.16 worktree
   to `repo-patched`, applied `bundle/coding/proposed_fix.diff` with
   `patch -p1` (applied cleanly to both files), and asserted the patched
   `ucp_login_link.php` no longer contains
   `get_provider($request->variable('auth_provider'` and now uses
   `get_provider()`.

2. **Patched image build.** Built `phpbb-cve2026-48611:patched` with the same
   Dockerfile/installer config as the repro (php:8.2-apache, gd/intl/zip, SQLite,
   official CLI installer creating the `admin`/`adminadmin` founder as
   `user_id=2`, default `auth_method=db`).

3. **Confirmed the patched code runs in the container** by grepping the file
   inside the running container for the no-arg `get_provider()`.

4. **TEST A — original exploit against the PATCHED build (must be blocked).**
   ```
   POST /ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1
   Authorization: Basic base64(admin:x)
   Content-Type: application/x-www-form-urlencoded
   login_username=admin&login_password=x&login=Login
   ```
   Result: `HTTP/1.1 200 OK`, `Set-Cookie: phpbb3_..._u=1` (anonymous), and a
   `<div class="error">You have specified an incorrect password. ...</div>`
   block. **No `_u=2`, no redirect to index.** The `db` provider verified the
   password and rejected `x`; the `apache` password-less sink was never reached.

5. **TEST B — legitimate login-link login against the PATCHED build (must still
   work).** Same request shape, but with the correct admin password in the body
   (`login_password=adminadmin`):
   ```
   POST /ucp.php?mode=login_link&auth_provider=apache&login_link_aikido=1
   Authorization: Basic base64(admin:x)        (ignored by the db provider)
   login_username=admin&login_password=adminadmin&login=Login
   ```
   Result: `HTTP/1.1 302 Found`,
   `Set-Cookie: phpbb3_..._u=2` (admin), `Location: index.php?sid=...`. The
   index page loaded with that session shows the admin username and the
   admin-only `Administration Control Panel` / `adm/index.php` link (count = 2).
   **Correct credentials still log in via the login-link flow — no regression.**

6. **CONTROL — original exploit against the UNPATCHED `:vuln` image (must still
   hijack).** The identical TEST A request against the unpatched 3.3.16 image:
   Result: `HTTP/1.1 302 Found`, `Set-Cookie: phpbb3_..._u=2` (admin),
   `Location: index.php?sid=...`. **The same exploit still hijacks `admin`
   (`user_id=2`) on unpatched 3.3.16**, proving the test harness reaches the real
   Apache+mod_php boundary and that the patch — not a broken exploit — is what
   blocks the attack on the patched build.

### Commands and output (verbatim, from `bundle/logs/verify_fix.log`)
```
patching file phpBB/includes/ucp/ucp_login_link.php
patching file phpBB/includes/ucp/ucp_register.php
patched ucp_login_link.php confirmed inside container (no-arg get_provider())
TEST A: session *_u = 1 (admin is 2); error block = 1; redirect-to-index = 0
TEST B: session *_u = 2 (admin is 2); redirect-to-index = 1
TEST B: admin indicators on index page (ACP link count) = 2
CONTROL: session *_u = 2 (admin is 2)
VERDICT: exploit_blocked_on_patched=yes; legit_login_works_on_patched=yes; control_hijack_on_unpatched=yes
RESULT: PASS — CVE-2026-48611 fix verified (exploit blocked on patched 3.3.16, legitimate login-link login still works)
```

## Test Results

| Test | Build | Request | `*_u` cookie | Outcome |
|---|---|---|---|---|
| TEST A (exploit) | patched 3.3.16 | `login_password=x` + `auth_provider=apache` + Basic `admin:x` | `1` (anonymous), 200 + error block | **Exploit blocked** ✓ |
| TEST B (legit) | patched 3.3.16 | `login_password=adminadmin` + `auth_provider=apache` + Basic `admin:x` | `2` (admin), 302 + ACP link | **No regression** ✓ |
| CONTROL (exploit) | unpatched 3.3.16 | `login_password=x` + `auth_provider=apache` + Basic `admin:x` | `2` (admin), 302 | **Still hijacks** ✓ (harness sound) |

`bundle/coding/verify_result.json` records `fix_verified: true`,
`test_a_exploit_blocked: true`, `test_b_legit_login_works: true`,
`control_hijack_on_unpatched: "yes"`.

**Edge cases considered / verified:**
- The attacker can still send `auth_provider=apache` (GET) and the Basic header;
  after the fix both are simply ignored — the board-configured `db` provider is
  used, the password is verified, and the wrong password is rejected. (This is
  TEST A.)
- The legitimate login-link flow is not broken: a correct password still
  produces an authenticated admin session through the same endpoint. (This is
  TEST B.)
- The fix is behavior-preserving for OAuth-configured boards: when
  `auth_method=oauth`, `get_provider()` returns the `oauth` provider exactly as
  the previous `get_provider('oauth')` did (the OAuth provider sets
  `auth_provider=oauth` in its redirect at `phpbb/auth/provider/oauth/oauth.php`
  when forwarding to the login-link/register flow), so OAuth account linking is
  unaffected.
- The patch applies cleanly with `patch -p1` and was confirmed present inside the
  running container.

## Remaining Concerns

- **Scope of the minimal fix vs. the official 3.3.17 release.** The official
  phpBB 3.3.17 fix additionally moved the login-link handling out of
  `ucp.php`/`includes/ucp/ucp_login_link.php` into a new Symfony-style
  controller `phpbb/ucp/controller/oauth.php` and deleted the legacy module.
  That architectural refactor is not security-relevant — the security fix is
  solely the removal of the attacker-controlled `auth_provider` argument, which
  this patch reproduces exactly. Boards that prefer the upstream-identical
  implementation should upgrade to phpBB 3.3.17+; this patch is intended as a
  minimal backport-style fix for 3.3.0–3.3.16.
- **`apache` provider trust model.** The `apache` provider intentionally trusts
  `PHP_AUTH_USER` (it is designed for boards fronted by Apache/LDAP
  front-authentication). This patch does not alter that provider; it only
  prevents it from being selected through the login-link/register flows on
  boards that did not configure it. A board that deliberately sets
  `auth_method=apache` (the intended use case) is out of scope of this CVE and is
  unaffected. Optional defense-in-depth: gate the `apache` provider's trust model
  so it can only return `LOGIN_SUCCESS` when the board is actually configured for
  Apache/LDAP front-auth.
- **Residual anti-pattern fully closed.** The variant analysis identified
  `ucp_register.php` as the only other request-steerable `get_provider()` site
  in the codebase; this patch closes it. A whole-repo search confirms no
  remaining `get_provider($request->variable('auth_provider'` call sites after
  the patch (the only other `get_provider()` calls in `auth.php`, `session.php`,
  `functions.php`, `ucp_auth_link.php`, and `ucp_register.php:607` already pass
  no argument).
- **Regression test recommended.** Add a permanent 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 grant `LOGIN_SUCCESS` through those flows on a default
  `db` board.

## Artifacts

- `bundle/coding/proposed_fix.diff` — the unified diff (applies with `patch -p1`).
- `bundle/coding/verify_fix.sh` — idempotent verification script (exit 0 = pass).
- `bundle/coding/verify_result.json` — machine-readable verdict.
- `bundle/coding/verify_artifacts/` — captured HTTP responses and cookie jars:
  `testA_response.txt` (exploit on patched → blocked), `testB_response.txt` +
  `testB_index_with_session.html` (legit login on patched → admin session), and
  `control_response.txt` (exploit on unpatched → admin hijack).
- `bundle/logs/verify_fix.log` — annotated verification run log.
- `bundle/logs/verify_build_patched.log` — patched image build log.
