Forums Bug Reports Thread

Gate paywall (middleware path) emits an empty CSRF token from a mis-keyed session read, breaking checkout

Patrick Bass · Jun 6 · 15 · 1 Locked
[Major] [High Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 8:05pm

Area: Cross-cutting infra (audit p14) · Surface: GateMiddleware::renderPaywall -> /gate/checkout · Dimension: security · Severity: major

The inline 402 paywall rendered by GateMiddleware (shown when a user hits a read_gated surface) reads the CSRF token from the wrong session key, so it always renders an empty hidden `_csrf_token`. When the user clicks Subscribe, GateController::checkout() calls validateCsrf(), which hash_equals the real (non-empty) token against the empty submitted value and 403s. The subscribe/checkout button on the inline paywall is therefore broken for every user — a direct revenue/availability defect on the monetization surface. (The standalone GET /gate/paywall page works because GateController::paywall() uses $this->render(), which injects the correct csrfToken; only the middleware-injected paywall is broken.)

Evidence

platform/src/Middleware/GateMiddleware.php:89 — `$csrfToken = $_SESSION['_csrf_token'] ?? '';`. The canonical session key is `csrf_token` (no leading underscore): BaseController.php:425-428 generates `$_SESSION['csrf_token']`, and validation reads `$_SESSION['csrf_token']` (CsrfValidationMiddleware.php:41, BaseController.php:491-493). `grep -rn "_SESSION\['_csrf_token'\]" src/ templates/` returns this single line. The paywall template embeds it: templates/gate/paywall.php:82 `<input type="hidden" name="_csrf_token" value="<?= $e($csrfToken) ?>">`, and the target handler enforces CSRF: GateController.php:30-31 `requireAuth(); validateCsrf();`.

Suggested fix. In GateMiddleware::renderPaywall(), read the correct key and generate one if absent, mirroring BaseController::csrfToken(): `if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } $csrfToken = $_SESSION['csrf_token'];`.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:25am

Resolved — fixed and deployed. Commit dd336ac47616, shipped dev-first then to all tenants on 2026-06-06.

renderPaywall() was reading the wrong session key ($_SESSION['_csrf_token'], which is never populated), producing an empty checkout-form token. Now mirrors BaseController::csrfToken(): generates $_SESSION['csrf_token'] if absent and uses it, so POST /gate/checkout passes CSRF validation.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.