passbolt/passbolt_api · error · BadRequestException

CSRF issue. The state in request data does not match with…

Error message

CSRF issue. The state in request data does not match with cookie value.

What it means

CSRF check for the POST-based SSO endpoints: the 'state' field in the request body must match the state stored in the SSO cookie. The controller compares them and throws a 400 on mismatch, preventing forged or replayed SSO requests.

Solutions

  1. Ensure the request body's 'state' field is taken from the same value issued in the SSO_COOKIE at flow start.
  2. Restart the flow so the cookie and the request data come from the same attempt.
  3. In automated clients, persist the state value issued in stage1 and send it verbatim in stage2.
  4. Verify Content-Type and body encoding so getData('state') actually parses (JSON vs form data).

Example fix

// before
await fetch('/sso/stage2', {method:'POST', body: JSON.stringify({code})});
// after
await fetch('/sso/stage2', {method:'POST', body: JSON.stringify({code, state: savedState})});
Defensive patterns

Strategy: validation

Validate before calling

const state = issuedStateAtStage1;
if (typeof state !== 'string' || state.length === 0) { throw new Error('State not captured at stage1'); }
payload.state = state; // send verbatim in stage2 body

Type guard

function hasStateInBody(body, cookieState) { return typeof body?.state === 'string' && body.state === cookieState; }

Try / catch

try { await api.post('/sso/stage2', {code, state}); } catch (e) { if (e.status === 400 && /state in request data/.test(e.message)) { restartSsoFlow(); } else { throw e; } }

Prevention

When it happens

Trigger: POST to an SSO endpoint (e.g. stage2 code verification via request data) where getData('state') differs from getStateFromCookie(), including when the cookie is absent or the client omits/mistypes the state field.

Common situations: API client sending state from an old session while holding a newer cookie; missing state field in the JSON body; test harness that sets the cookie but not the payload state (or vice versa); two parallel SSO flows in one browser.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/dd2876b98bc6200f. Report an issue: GitHub.

Appendix: source

Thrown at plugins/PassboltEe/Sso/src/Controller/AbstractSsoController.php:62

        if ($stateUrl !== $stateCookie) {
            throw new BadRequestException(__('CSRF issue. The state in URL and Cookies do not match.'));
        }

        return $stateUrl;
    }

    /**
     * Protect from CSRF by checking if state in URL and cookie matches
     *
     * @throws \Cake\Http\Exception\BadRequestException if the state is not provided in cookie or URL or there is a mismatch
     * @return string
     */
    public function getStateAndAssertAgainstCookie(): string
    {
        $state = $this->getRequest()->getData('state');
        $stateCookie = $this->getStateFromCookie();
        if ($state !== $stateCookie) {
            throw new BadRequestException(
                __('CSRF issue. The state in request data does not match with cookie value.')
            );
        }

        return $state;
    }

    /**
     * @throws \Cake\Http\Exception\BadRequestException if the state is not provided in cookie or invalid type
     * @return string state
     */
    public function getStateFromCookie(): string
    {
        $state = $this->request->getCookie(AbstractSsoService::SSO_STATE_COOKIE);
        if (!is_string($state) || !SsoState::isValidState($state)) {
            throw new BadRequestException(__('The state is required in cookie.'));
        }

View on GitHub (pinned to 31c1bbc10f)