security/csrf
< >

return function () {
    $self = url('/@object/@action/' . urlencode('security/csrf'));
    $form_id = 'docs_csrf_demo';
    $intro = '<p class="docs-demo-intro">CSRF protection of form2: with the <code>csrf</code> option the form carries a one-time
        <code>_token</code> that <code>yf_csrf_guard</code> keeps per form in the session. <b>Send with the token</b> passes; <b>Send without a
        token</b> posts the same fields (with this form\'s <code>__form_id__</code>) minus the token, as a forged cross-site form would,
        and is refused; <b>Replay the used token</b> resends a token that was already accepted and is refused too, because
        <code>validate()</code> consumes it. Nothing is stored. Classes: <code>yf_form2</code>, <code>yf_csrf_guard</code>.</p>';
    $result = '';
    $form = form((array) $_POST, ['csrf' => true, '__form_id__' => $form_id])
        ->validate(['__form_id__' => $form_id, 'message' => 'trim|required|max_length[200]'])
        ->on_validate_ok(function ($data) use (&$result) {
            // the token was valid (a CSRF failure adds a validation error, so this is not reached then)
            $_SESSION['docs_csrf_demo_used'] = (string) ($_POST['_token'] ?? '');
            $result = '<div class="alert alert-success"><b>Accepted.</b> The token matched and is now used up. Message: '
                . _prepare_html((string) $data['message'], 0) . '</div>';
        })
        ->text('message', ['value' => 'Hello', 'maxlength' => 200])
        ->submit(['value' => 'Send with the token']);
    // A failed CSRF check logs into the log_csrf_errors table; the demo writes no rows
    $guard = _class('csrf_guard');
    $log_errors = $guard->LOG_ERRORS;
    $guard->LOG_ERRORS = false;
    try {
        $html = (string) $form;
    } finally {
        $guard->LOG_ERRORS = $log_errors;
    }
    if (is_post() && $result === '' && ($_POST['__form_id__'] ?? '') === $form_id) {
        $result = '<div class="alert alert-danger"><b>Refused.</b> The form was posted without a valid token; form2 showed the error and ran no on_validate_ok() handler.</div>';
    }
    $forged = '<form method="post" action="' . $self . '" class="docs-demo-forged">'
        . '<input type="hidden" name="__form_id__" value="' . $form_id . '"><input type="hidden" name="message" value="Forged without a token">'
        . '<button type="submit" class="btn btn-danger">Send without a token</button></form>';
    $used = (string) ($_SESSION['docs_csrf_demo_used'] ?? '');
    $replay = $used === '' ? '' : '<form method="post" action="' . $self . '" class="docs-demo-forged">'
        . '<input type="hidden" name="__form_id__" value="' . $form_id . '"><input type="hidden" name="message" value="Replayed">'
        . '<input type="hidden" name="_token" value="' . _prepare_html($used, 0) . '">'
        . '<button type="submit" class="btn btn-warning">Replay the used token</button></form>';
    css('.docs-demo-forged { display:inline-block; margin:0 6px 15px 0; }');
    return $intro . $result . $html . '<h4>Forged requests</h4>' . $forged . $replay;
};

CSRF protection of form2: with the csrf option the form carries a one-time _token that yf_csrf_guard keeps per form in the session. Send with the token passes; Send without a token posts the same fields (with this form's __form_id__) minus the token, as a forged cross-site form would, and is refused; Replay the used token resends a token that was already accepted and is refused too, because validate() consumes it. Nothing is stored. Classes: yf_form2, yf_csrf_guard.

Forged requests