We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

CSRF validation failed

Hello everyone. There was a CSRF validation failed Everything works fine if I do not use plug-in service.php

/**
 * Dispatcher use a default namespace
 */
$di->set('dispatcher', function() use ($di) {
    $eventsManager = $di->getShared('eventsManager');

    $security = new Security($di);

    /**
     * We listen for events in the dispatcher using the Security plugin
     */
    $eventsManager->attach('dispatch', $security);

    $dispatcher = new Dispatcher();
    $dispatcher->setEventsManager($eventsManager);
    $dispatcher->setDefaultNamespace('EasyPay\Controllers');

    return $dispatcher;
});

Security.php

/**
 * This action is executed before execute any action in the application
 */
public function beforeDispatch(Event $event, Dispatcher $dispatcher)
{
    $auth = $this->session->get('auth-identity');

    if (!$auth) {
        if ($dispatcher->getControllerName() != 'session') {
            $dispatcher->forward(array(
                'controller' => 'session',
                'action' => 'login'
            ));

            return false;
        }
    }
}


42.1k

LoginForm.php

...
//CSRF
$csrf = new Hidden('csrf');

$csrf->addValidator(
    new Identical(array(
        'value' => $this->security->getSessionToken(),
        'message' => 'CSRF validation failed'
    ))
);

$this->add($csrf);

login.volt

...
{{ form.render('csrf', ['value': security.getToken()]) }}

When you open the login page in the value field 'csfr' is not equal to what is in session



8.1k

Your generated html code of form must contain CSRF string like

<input type="hidden" name="ReJLqVYMXeHVieg9" value="c3b71a6ca4e79b35257b614684705bc8"/>

This string generated from code like

<input type="hidden" name="<?php echo $this->security->getTokenKey() ?>" value="<?php echo $this->security->getToken() ?>"/>

You may to put to your form view 2 elemets : 1) name of hidden csrf field by php getTokenKey() 2) and value of token by php getToken()



42.1k

Alas, it did not solve my problem



42.1k

The problem remains the same. After sending the post data $this->security->getSessionToken() returns a different value, not what's in the hidden field. Note that this problem only occurs when running plugin.



8.1k

Quote:

/*
* LoginForm.php
*/
$csrf = new Hidden('csrf');

Try replace this to


$csrf = new Hidden(array(
     'name'    => $this->security->getTokenKey(),
     'value'    => $this->security->getToken(),
));```
Meaning - 
when Security class initializing csrf token, this token is writing to session in special name, that generated once in session by Security class ( function getTokenKey ).
That is session not have key 'csrf' with Token, but have key with name Security->getTokenKey() generated and with value Security->getToken()
Call var_dump of $_SESSION and you see.
You use key 'csrf' - this is erroneous decision.
And note for the html code that is generated, you may see input hidden with generic name and value from Security class.


42.1k

//Олег Вы говорите по русски?



8.1k


42.1k

Олег я сделал как Вы рекомендовали. Все равно в сессиях каким-то образом записывается другой хеш, нежели тот что в хайден поле. Заметьте что это только если я использую свой плагин, т.е. без этого плагина все прекрасно работает. Что-то с этими событиями диспетчера.



8.1k

1) Я не понимаю, зачем в service.php вы создаете новый security объект и никуда его не возвращаете. Security - это самостоятельный сервис, к тому же автомаnически инициализируется. Переназначение нужно только для изменения пераметров. 2) Зачем проверять csrf формы в beforeDispatch? Достаточно в действии контроллера проверить

if ( $this->request->isPost() && $this->security->checkToken() ) {
...
}

3) Попробуйте сделать поле csrf в форме напрямую (см. выше), и в принимающем действии отследите входные данные запроса и данные сессии.



42.1k

Мой Sercurity класс это не совсем тот о котором Вы говорите. Речь идет о https://docs.phalcon.io/en/latest/reference/tutorial-invo.html#events-management Задача состоит в том, чтобы защитить приложение от не авторизованных пользователей.



8.1k

Я делаю приложения всегда мультимодульные, так уж получилось. Да и не вижу смысла в другом. Все равно есть бэкенд и фронтенд. Ну вот у себя мне нужно закрыть один модуль. В нем есть фронтенд контроллер и контроллер пользоватлей. В последнем, при логировании пользователя я в форму включаю csrf тем способом, который описал вам выше. При успешном логировании я инициализирую сессию

public function loginAction() {
      ....
    if ( $this->request->isPost() && $this->security->checkToken() ) {
   //if user ok   
            $this->_registerSession($user);
}
private function _registerSession($user) {
    $this->session->set('auth', [
          'id' => $user->id,
          'name' => $user->name,
          ]);
  }

Далее, в защищаемом контроллере я использую


public function beforeExecuteRoute($dispatcher) {
    $auth = $this->session->get('auth');
    ....
}
...

Б.... , я кажется понял вашу ошибку. Вы пытаетесь опереться на данные сессии в части генерируемого csrf, может быть. Но здесь есть одно НО. При каждом запросе данные csrf сессии генерируются заново и никогда не будут совпадать с сохраненным значением. В этом смысл csrf защиты.
Т.е. по запросу мы выводим форму  - в нее генерируется поле csrf и заносится в сессию. При обработке формы мы проверяем токен при помощи функции checkToken().
Т.е. если вы будете проверять сессию по генерируемому csrf, то при каждом запросе получите новые данные.
Вот как-то так. Насколько я увидел, в INVO приблизительно также решается этот вопрос.


42.1k

У меня не мультимодульное приложение, и я хочу контролировать доступ пользователей в одном месте, поэтому я использую плагин Security. Проблема в том, что когда я его использую, валидация csfr не проходит. Без плагина все работает нормально (оба варианта, мой и Ваш). Таже проблема и с ControllerBase с его методом beforeExecuteRoute. Что же здесь не так?



8.1k

А что вы пишете в сессию, и что проверяете? Просто csrf проверятся только для формы, для контроля доступа используется ACL. Т.е. когда пользователь прошел проверку, его данные нужно сохранить в сессии, и далее использовать эти данные. И csrf использовать здесь уже не стоит. Т.е. валидация csrf отрабатывает только при обработке формы. Больше мне нечего сказать. Можете выложить код на github, когда будет время, я посмотрю. Но ничего гарантировать не могу.



42.1k

Вы все верно говорите. Так дело в том что csrf я использую для валидации формы, мне он больше не нужен. После авторизации пользователя, в сессию я записываю его id и name. За основу проекта используется код проекта vokuro



8.1k

Форма у вас проходит проверку? Если нет, тогда покажите, как генерируете форму, код html на выходе (для формы) и код функции проверки. А то я уже несколько запутался, что именно у вас не работает. Началось у вас с CSRF validation failed.



42.1k

views/session/login.volt

<!-- Login Formular -->
{{ form('class': 'form-vertical login-form') }}
    <!-- Title -->
    <h3 class="form-title">Вход в личный кабинет</h3>

    <!-- Error Message -->
    {{ content() }}
    <div class="alert fade in alert-danger" style="display: none;">
        <i class="icon-remove close" data-dismiss="alert"></i>
        Enter any username and password.
    </div>

    <!-- Input Fields -->
    <div class="form-group">
        <!--<label for="username">Username:</label>-->
        <div class="input-icon">
            <i class="icon-envelope"></i>
            {{ form.render('email', ['class': 'form-control', 'placeholder': 'Электронная почта', 'autofocus': 'autofocus', 'data-rule-required': 'true', 'data-msg-required': 'Введите адрес электронной почты']) }}
        </div>
    </div>
    <div class="form-group">
        <!--<label for="password">Password:</label>-->
        <div class="input-icon">
            <i class="icon-lock"></i>
            {{ form.render('password', ['class': 'form-control', 'placeholder': 'Пароль', 'data-rule-required': 'true', 'data-msg-required': 'Введите пароль']) }}
        </div>
    </div>
    <!-- /Input Fields -->

    <!-- Form Actions -->
    <div class="form-actions">
        <label class="checkbox pull-left">{{ form.render('remember', ['class': 'uniform']) }} {{ form.getLabel('remember') }}</label>
        <button type="submit" class="submit btn btn-primary pull-right">
            Войти <i class="icon-angle-right"></i>
        </button>
    </div>
    {{ form.render('csrf', ['value': security.getToken()]) }}
</form>
<!-- /Login Formular -->

controllers/SessionController.php

if (!$this->request->isPost()) {
if ($form->isValid($this->request->getPost()) == false) {
                    foreach ($form->getMessages() as $message) {
                        $this->flash->error($message);
                    }
                } else {
                    $this->auth->check(array(
                        'email' => $this->request->getPost('email'),
                        'password' => $this->request->getPost('password'),
                        'remember' => $this->request->getPost('remember')
                    ));

                    return $this->response->redirect('index');
                }
}


42.1k

У меня форма не проходит валидацию



8.1k

Покажите код html, который генерирует views/session/login.volt , часть в тэгах <form></form>



42.1k

forms/Session/Login.php

class Login extends Form
{

    public function initialize()
    {
        //Email
        $email = new Text('email', array(
            'placeholder' => 'Email'
        ));

        $email->addValidators(array(
            new PresenceOf(array(
                'message' => 'The e-mail is required'
            )),
            new Email(array(
                'message' => 'The e-mail is not valid'
            ))
        ));

        $this->add($email);

        //Password
        $password = new Password('password', array(
            'placeholder' => 'Password'
        ));

        $password->addValidator(
            new PresenceOf(array(
                'message' => 'The password is required'
            ))
        );

        $this->add($password);

        //Remember
        $remember = new Check('remember', array(
            'value' => 'yes'
        ));

        $remember->setLabel('Запомнить');

        $this->add($remember);

        //CSRF
        $csrf = new Hidden('csrf');

        $csrf->addValidator(
            new Identical(array(
                'value' => $this->security->getSessionToken(),
                'message' => 'CSRF validation failed'
            ))
        );

        $this->add($csrf);
    }
}

Это оно?



8.1k

Когда у вас форма отображается в браузере нажмите Ctrl+U , в соседней вкладке получите код страницы. Оттуда скопируйте сюда содержимое между <form> и </form>



42.1k

<form class="form-vertical login-form" method="post"> <!-- Title --> <h3 class="form-title">Вход в личный кабинет</h3>

<!-- Error Message -->
    <div class="alert fade in alert-danger" style="display: none;">
    <i class="icon-remove close" data-dismiss="alert"></i>
    Enter any username and password.
</div>

<!-- Input Fields -->
<div class="form-group">
    <!--<label for="username">Username:</label>-->
    <div class="input-icon">
        <i class="icon-envelope"></i>
        <input placeholder="&#x42d;&#x43b;&#x435;&#x43a;&#x442;&#x440;&#x43e;&#x43d;&#x43d;&#x430;&#x44f; &#x43f;&#x43e;&#x447;&#x442;&#x430;" class="form-control" autofocus="autofocus" data-rule-required="true" data-msg-required="&#x412;&#x432;&#x435;&#x434;&#x438;&#x442;&#x435; &#x430;&#x434;&#x440;&#x435;&#x441; &#x44d;&#x43b;&#x435;&#x43a;&#x442;&#x440;&#x43e;&#x43d;&#x43d;&#x43e;&#x439; &#x43f;&#x43e;&#x447;&#x442;&#x44b;" name="email" id="email" value="a.farkhod&#x40;gmail.com" type="text" />     </div>
</div>
<div class="form-group">
    <!--<label for="password">Password:</label>-->
    <div class="input-icon">
        <i class="icon-lock"></i>
        <input placeholder="&#x41f;&#x430;&#x440;&#x43e;&#x43b;&#x44c;" class="form-control" data-rule-required="true" data-msg-required="&#x412;&#x432;&#x435;&#x434;&#x438;&#x442;&#x435; &#x43f;&#x430;&#x440;&#x43e;&#x43b;&#x44c;" name="password" id="password" value="password" type="password" />       </div>
</div>
<!-- /Input Fields -->

<!-- Form Actions -->
<div class="form-actions">
    <label class="checkbox pull-left"><input class="uniform" name="remember" id="remember" value="yes" type="checkbox" /> Запомнить</label>
    <button type="submit" class="submit btn btn-primary pull-right">
        Войти <i class="icon-angle-right"></i>
    </button>
</div>
<input value="22a39a9c1aecd158af1c9ee5fa0b813d" name="csrf" id="csrf" type="hidden" /></form>


8.1k

Цитата:

<input value="22a39a9c1aecd158af1c9ee5fa0b813d" name="csrf" id="csrf" type="hidden" /></form>

я объяснял выше - именно поэтому у вас форма не работает. Класс Security генерирует ПАРУ скрытого поля csrf - имя => значение. Сгенрированный код формы выглядит пимерно так

<form class="userform" method="post" action="#">
<input type="hidden" name="dTSUWVpKSDjKRfsw" value="4ec47f99a7999633a418b61d88a5e4c6"/>
<div>
<label for="email">Email</label>
<input type="text" size="24" placeholder="email" value="" maxlength="32" name="email" id="email" />
</div>
<div>
<label for="password">Password</label>
<input type="password" size="24" placeholder="password" value="" maxlength="16" name="password" id="password" />
</div>
<div>
<input type="submit" class="btn" value="Войти" style="width:auto;">
</div>
</form>

Обратите внимание на поле Hidden с "мудреным" именем и значением - это и есть поле CSRF.



13.8k

Олег, меня интересует следующий вопрос! Почему в демо приложениях используют также как выше в примере? Т.е. у инпута name = csrf, а value = токен? У меня на локалхосте в 3 приложениях формы работают без проблем именно так...но вот в последнем приложении в упор понять не могу почему не работает и вываливается с ошибкой валидации :( токен в сессии и в форме разные :( почему разные - не могу ответить сам!!



8.1k

Честно говоря, я в демо-приложениях смотрю только код. А дальше уже самостоятельно пробую функциональность. Заметил, что со времени первого анонса демо-приложения не изменились (по-моему), поэтому полезность их, наверное, только в том, чтобы посмотреть (как говорят "покурить") код. Но это мое личное мнение. По поводу csrf -

  • csrf токены генерируются сервисом и сохраняются в сессии - это факт :)
  • при обработке формы нам необходимо сравнить токен в сессии с токеном из запроса
  • в сессии токен хранится в виде пары ключ-значение, где ключ - это getTokenkey, значение - getToken. Эту пару и следует проверять. Поэтому, несмотря на примеры, я пришел к выводу, что наиболее логично генерировать пару Tokenkey => getToken в контроллере, передавать эти данные в форму. Затем проверять csrf при помощи checkToken. Тогда это работает. И, главное, я это понимаю. :) А почему там в примерах по-другому...? Я эти примеры посмотрел только после вашей ссылки на них. Поэтому даже не знаю, работает ли там такой подход. Я их не пробовал. А вот без фреймворков, просто на PHP, делал csrf защиту по такой-же схеме (только сессии использовал на Redis) и стараюсь не отступать от нее. Но там было имя поля фиксированное, а phalcon сделал его динамическим - это большой плюс. Чтобы понять, прогоните (перехватите) полностью цепочку запросов и проанализируйте их. Это бывает неудобно, но для понимания необходимо.


13.8k

Понятно....переписал все формы по токен-ключ методу...собака все равно не работает :( впервые пишу модульное приложение с фронтендом и бэкендом...в контроллере пытаюсь посмотреть сессию

var_dump($_SESSION);

Получаю в ответ Undefined variable: _SESSION :( Хотя сессия типо стартуется(если удалить PHPSESSID то он создастся снова) В другом приложении не модульном все в порядке - работает...выводит содержимое сессии полностью :( походу в этом то и проблема...скрипт пытается проверить токен а его проверить не с чем :(

Также не понятно...$this->security->checkToken() принимает в качестве аргументов ключ и сам токен...но при валидации формы мы не можем знать ключ и токен...как принять из с POST'а? во всех примерах эта функция используется без аргументов почему то



8.1k

security->checkToken берет данные и из запроса, и из сессии. Поэтому и без параметров. Протестируйте, в конце концов, пример из документации. Он точно работает. Может вы инициализируете приложение неправильно? Посмотрите у меня на Github скелетон мультимодульного приложения, может, там что полезное найдете.



13.8k

Вообщем сессия не дампилась до инициализации формы...токен также не проверяется при инициализации :( Получается значит такая тема: при инициализации формы токен и ключ в сессии меняются, соответственно если инициализация формы проходит раньше чем вызов функции checkToken, то сверяет он уже совершенно новые данные :( Если же проверят токен до инициализации - выдаёт необходимое мне TRUE

public function loginAction()
    {
        var_dump($this->security->checkToken()); // TRUE
        $form = new LoginForm();
        echo '<pre>';
        echo '================= SESSION DUMP ===============<br>';
        var_dump($_SESSION);
        echo '====================== END ===================<br><br>';

            if (!$this->request->isPost()) {

                if ($this->auth->hasRememberMe()) {
                    return $this->auth->loginWithRememberMe();
                }

            } else {
                echo '================= GETTED POST ================<br>';
                var_dump($this->request->getPost());
                echo '===================== END ====================<br><br>';
                echo 'checkToken: ';
                var_dump($this->security->checkToken()); // FALSE

                if($this->security->checkToken()) {
                    if ( $form->isValid($this->request->getPost()) == false ) {
                        var_dump('FORM NOT VALID');
                        foreach ($form->getMessages() as $message) {
                            $this->flashSession->error($message);
                        }
                    } else {
                        var_dump('FORM VALID');

                        // $this->auth->check(array(
                        //  'login' => $this->request->getPost('login'),
                        //  'password' => $this->request->getPost('password'),
                        //  'remember' => $this->request->getPost('remember')
                        // ));

                        //return $this->response->redirect('dashboard');
                    }
                } else
                    $this->flashSession->error("[CSRF] Ошибка валидации формы");

            }
        echo '</pre>';
        $this->view->form = $form;
    }

В форме использую

$csrf = new Hidden('csrf', array(
    'name' => $this->security->getTokenKey(),
        'value' => $this->security->getToken()
));
$this->add($csrf);

Полагаю как вариант CSRF инициализировать во вьюхе...просто в таком виде невозможно использовать в экшене, которые также отвечает за обработку POST



8.1k

По-моему, правильно. Так и должно быть. Мне кажется, у вас ошибка в логике. Вы сначала генерируете форму, не проверяя тип запроса. Т.е. у вас в любом случае сначала обрабатыватеся любой запрос с генерацией формы и нового csrf, а потом уже идет обработка POST запроса. Попробуйте изменить логику:

public function loginAction() {
     if ( $this-request->isPost() ) {
          // handle Post request with csrf validation
    } else {
         // handle Get request with Form generation
    }
}

как-то так. Посмотрите пример еще здесь https://habrahabr.ru/post/197254/ Поле csrf можете добавить в класс EntityForm в initialize перед кнопкой отправки.



8.1k

Появилось время, залил пример на github - там введена CSRF защита. https://github.com/oleg578/PhalconAnnotationsExample



13.8k

Посмотрел пример:

  • для чего переопределять сервис security?
  • понял что формы по сути повторяют поля моделей с условиями аннотаций...позволяет ли такой подход использовать динамичность в формах? надеюсь верно выражаюсь))


8.1k

Формы генерируются по правилам, определенным нами в аннотациях. Аннотации определяем в моделях. Общий класс обрабатывает любую модель. Отсюда делайте выводы. Security сервис определяю автоматом... С CSRF вам уже понятно?



13.8k

Да, огромное спасибо :))



8.1k

Ну вот и хорошо. Пожалуйста.