yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.24k stars 6.91k forks source link

Yii2 (v 2.0.16-dev) Randomly fail to validate _csrf token on login. #16780

Open giovaz94 opened 6 years ago

giovaz94 commented 6 years ago

What steps will reproduce the problem?

  1. Application is Yii2 Advanced Template
  2. On configurations have these settings:
'user' => [
            'identityClass' => 'backend\models\User',
            'enableAutoLogin' => false,
            'identityCookie' => [
                'name' => '_cookieBackend', // unique name
            ],
            'loginUrl' => ['site/login']
        ],

Also I have 'cookieValidationKey' property setted :

'components' => [
        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => 'SOMEKEY',
        ],
    ],
  1. Creating form for Login with ActiveForm (method POST) and Login

What is the expected result?

Login in the application.

What do you get instead?

[yii\web\HttpException:400] yii\web\BadRequestHttpException: Unable to verify your data submission.

Additional info

Q A
Yii version 2.0.16-dev
PHP version 5.6.30
Operating system

This issue appear Randomly, sometimes I can login successfully and sometimes not. On getting the error reload resolve the problem; I'm redirected inside the application and the user is logged. Disabling csrfValidation resolve the problem but it compromise the application security.

I dumped the function that validate the token:

/**
     * Validates CSRF token.
     *
     * @param string $clientSuppliedToken The masked client-supplied token.
     * @param string $trueToken The masked true token.
     * @return bool
     */
    private function validateCsrfTokenInternal($clientSuppliedToken, $trueToken)
    {
        if (!is_string($clientSuppliedToken)) {
            return false;
        }

        $security = Yii::$app->security;

        var_dump($security->unmaskToken($clientSuppliedToken));
        echo "<br \>";
        echo "<br \>";
        var_dump($security->unmaskToken($trueToken));
        echo "<br \>";
        echo "<br \>";
        var_dump($security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken)));
        die;

        return $security->compareString($security->unmaskToken($clientSuppliedToken), $security->unmaskToken($trueToken));
    }

In some cases variables $clientSuppliedToken and $trueToken have different values causing validateCsrfTokenInternal to return FALSE.

Also reporting this similar issue: 353181592

samdark commented 6 years ago

You should use separate csrf cookie additionally to identity cookie.

giovaz94 commented 6 years ago

Added configuration for csrfCookie:

'request' => [
     'csrfParam' => '_sitenameCSRF', // Modified params
     'csrfCookie' => [
           'httpOnly' => true,
           'path' => '/backend',
     ],
 ],

Problem is still occurring. validateCsrfTokenInternal can't verify tokens because $clientSuppliedToken() and $trueToken() are different.

samdark commented 6 years ago

Do you have any AJAX calls?

giovaz94 commented 6 years ago

Yes, I've got a Ajax call in the page after the login but is handled with _csrf :

$.ajax({
                    url: '".$dashUrl"',
                    cache: false,
                    method: 'POST',
                    data: { uuid : window.name, _sitenameCSRF: yii.getCsrfToken() },
                    dataType: 'json'
            });

Problem is still persisting.

giovaz94 commented 6 years ago

I know that error is throwed on line 166 of yii/web/Controller in the beforeAction() method but , being a 400 error should be a client error ? List of status code.

Trying to login and logout multiple times with different Browsers I've noticed that on 10 attempts the error shows up:

Safari 12.0 => 1 time on 10 attemps (with clear cache and all cookies deleted), Google Chrome 69.0.3497.100 => 4 time on 10 attemps (with clear cache and all cookies deleted), Mozilla Firefox 62.0.3 => 3 time on 10 attemps (with clear cache and all cookies deleted),

I don't know if this information is relevant considering that Client and Internal csrf token is generated by Yii.

samdark commented 6 years ago

Well, I'm afraid we can do nothing about it since we don't know how to reproduce it.

samdark commented 6 years ago

That could be some kind of pre-fetch from the browser that asyncronously refreshes the token...

njasm commented 6 years ago

just to confirm that we are also experience the exact same issue.

giovaz94 commented 6 years ago

@njasm yes, seems similar, don't know in your case if this can be also caused by the ajax sending wrong _csrf token but in that case i think you'll get a console error.

Anyway user randomly get the error but with a refresh of the page they seems to logged in. Don't know what cause this behavior.

giovaz94 commented 6 years ago

Ok, seems that the error is caused by regenerating _csrf token in login method on yii\web\User class on line 261.

I've overwriting this method and removed the line ( so it doesn't regenerate the token). This makes application doesn't cause the Bad Request error but also generate a security issue.

Any solution for this ?

samdark commented 6 years ago

Can you reproduce it w/o AJAX?

xutl commented 5 years ago

Consider the reason for prefetching of favicon.ico https://segmentfault.com/q/1010000004450797

Gruven commented 5 years ago

Yii2 2.0.15.1 Advanced Template. The same issue on login page. Can't reproduce, because saw it about two times for 3 months

s1lver commented 5 years ago

Yii2 2.0.15.1 PHP 7.2.10

Today, too, faced with this problem. This only plays on the server, I can't repeat locally.

samdark commented 5 years ago

Moving to 2.0.17 since it's not clear how to reproduce it and what causes it.

kartik-v commented 5 years ago

Confirm this issue on PHP v7.2.x on my server environment but does not happen on PHP 7.1.x on my local environment with the same latest yii2 version involving ajax call. Is it something to do with the PHP version?

kartik-v commented 5 years ago

Ah ok - I think I debugged the source of the problem. Its not CSRF but apparently the exit in PHP 7.2.x is different.

If you have a controller action for an ajax response that returns a JSON like this - you may get a bad request in PHP 7.2

public function actionAjaxOutput()
{
     echo \yii\helpers\Json::encode(['output' => 'something', 'message' => 'something']);
    // this will throw a bad request in PHP 7.2
}

Instead change the above code to something like below:

public function actionAjaxOutput()
{
     Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
     return ['output' => 'something', 'message' => 'something']; 
    // works fine in PHP 7.2.x
}
s1lver commented 5 years ago

I'm using that option. Everything is fine PHP 7.2.x

[
    'class' => ContentNegotiator::class,
    'formats' => [
        'application/json' => Response::FORMAT_JSON
    ]
],
YiiRocks commented 5 years ago

Always return in controller, echo will cause problems.

kab91 commented 5 years ago

Faced with same problem recently on login and signup pages.

[yii\web\HttpException:400] yii\web\BadRequestHttpException: Unable to verify your data submission.

This message shows up randomly in the log file for some users. My idea is that users has slow Internet connection and they hit "Login" button many times which causes CSRF to regenerate.

I made a small screencast how to reproduce the issue in the fresh Yii2 Advanced template install. Also I enabled Network Throttling = Slow 3G in the Chrome dev tools. Please take a look: http://recordit.co/a4XN1oASDR

Any ideas how to fix that?

YiiRocks commented 5 years ago

You could disabled the submit button with javascript

$('form').on('beforeValidate', function (e) {
    $(e.target).find(':submit').prop('disabled', true);
}).on('afterValidate', function (e) {
    $(e.target).find(':submit').prop('disabled', false);
});
kab91 commented 5 years ago

You could disabled the submit button with javascript

$(document).ready(function() {
    $("form#my_form").submit(function() {
        $('button[type=submit], input[type=submit]').prop('disabled',true);
        return true;
    });
});

Nice workaround, thank you!

vko-dp commented 5 years ago

Moving to 2.0.17 since it's not clear how to reproduce it and what causes it.

привет так же столкнулся с аналогичной проблемой Yii2 version 2.0.14.2 при первой загрузке страницы (т.е. пользователь впервые зашел)

vko-dp commented 5 years ago

возникает это вот почему: protected function regenerateCsrfToken() { $request = Yii::$app->getRequest(); if ($request->enableCsrfCookie || $this->enableSession) { $request->getCsrfToken(true); } }

здесь vendor/yiisoft/yii2/web/User.php происходит перегенерация токена и кука изменяется а токен в метатеге и скрытых полях форм остается старым

страница на которой это можно обнаружить должна отправлять аякс запросы

vko-dp commented 5 years ago

есть вопрос зачем в этом методе вызывается $request->getCsrfToken(true); с $regenerate = true ? пока что решил проблему перегрузив метод но хотелось бы знать что будет в будущих версиях чтоб не навредить

samdark commented 5 years ago

зачем в этом методе вызывается $request->getCsrfToken(true); с $regenerate = true ?

Чтобы нельзя было воспользоваться токеном отлогинившегося пользователя. См. #15783

vko-dp commented 5 years ago

привет)

зачем в этом методе вызывается $request->getCsrfToken(true); с $regenerate = true ?

Чтобы нельзя было воспользоваться токеном отлогинившегося пользователя. См. #15783

но дело в том что сейчас это создает проблемы только для "честных пользователей" в ситуациях которую я описал выше при первом заходе на страницу где после загрузки страницы происходят аякс запросы. значение куки изменяется в этом случае а токен в метатеге и скрытых инпутах остается старым.

наверное это не очень здорово так делать я вот поотлаживал и вижу что устаревшие пары кука + токен - все рабочие и с ними можно проходить цсрф защиту - т.е. для "недобросовестных юзеров" (для которых цсрф и включаем) проблем нет - один раз можно получить пару и спамить с ней сколько влезет.

вот на скринах как выглядит тест: https://monosnap.com/file/kKApRHVS2N7ykLkZMXXs3GLXRPk3DB - взял 4 пары кука + токен и прогнал через цсрф защиту https://monosnap.com/file/ithyKc1Cc1QSyHXi7T3edA6B3HaYkm - все 4 пары успешно проходят защиту

т.е. в текущей реализации перегенерация не добавляет стойкости цсрф защите но создает проблемы при штатном использовании сайта где она используется (только при первом посещении за сессию и если со страницы после готовности идут аяксы)

поэтому пока что выкрутились перегрузив этот метод: public function getCsrfToken($regenerate = false) чтобы перегенерации не происходило если токен есть в куках

pabloneruda1 commented 4 years ago

Same issue. Can this be related to a reverse proxy (such as Apache Traffic Server - cached page with form and old CSRF token)?

samdark commented 4 years ago

Absolutely. CSRF token can not be used with full page caching on proxy level.

samdark commented 4 years ago

Absolutely. CSRF can not be used with full page caching on proxy level.

pabloneruda1 commented 4 years ago

1) I reloaded CSRF token in form via AJAX call with timestamp parameter (to bypass the proxy cache):

         $.ajax('/customer/login?t=<?= time() ?>', {
             success: function (data) {
                 let token = $(data).find('form > input[name="' + yii.getCsrfParam() + '"]').val();
                 $('form > input[name="' + yii.getCsrfParam() + '"]').val(token);
             }
         });

2) I added hidden input in form view with date and time:

  Html::hiddenInput('timestamp', date('Y-m-d H:i:s'))

And date and time is actual. Form is not loaded from the cache. It is freshly generated and there are still problems with the CSRF token. I really don't know where the problem is.

samdark commented 4 years ago

It could be that something is doing more AJAX requests in background and triggering CSRF token regeneration so token is obsolete by the time you are using it.

pabloneruda1 commented 4 years ago

How to solve this: the user opens 2x login form in two tabs:

samdark commented 4 years ago

That is expected from CSRF protection. If you absolutely don't want this behavior then disable it.

pabloneruda1 commented 4 years ago

I don't want disable CSRF protection, this worked for me:

public function beforeAction($action)
{
    if($action->id === 'login' && Yii::$app->user->isGuest === false){
        $this->redirect('index')->send();
        exit;
    }

    return parent::beforeAction($action);
}

I hope this solves random CSRF token validation errors. We'll see ...

Slayvin commented 4 years ago

From time to time, I also encounter that bug on the login form. But what I'm worried about, is that the password is stored in plain text in the log file! See this issue #8763 for instance. The op has a "Bad Request exception" after submitting the login form, and inside the log file, we can clearly see the password in plain text.

I experience the same issue, here is a partial copy-paste of the log file :

2020-09-23 15:53:32 [192.168.1.101][-][-][error][yii\web\HttpException:400] exception 'yii\web\BadRequestHttpException' ... Stack trace:

0 ...

... 2020-09-23 15:53:32 [109.130.141.39][103][dhpo7eeq655jg9mn99dd68q3j5][info][application] $_GET = []

$_POST = [ '_csrf' => 'oZzuAvpt8vRwdG_sjg6ij4mCP7Mxm4v_oL--gWU4SUbIy7EwlFjAwTksW438ff325_dV_wboyq_uj8fHUAAkLQ==' 'LoginForm' => [ 'login' => 'user_AT_hotmail.com' 'password' => 'mySuperPasswordInPlainText' 'rememberMe' => '0' ] 'ajax' => 'LoginForm' ] ...

I know it's not a bug 'per se', but I think this can lead to a security issue. How can I prevent that to happen? How hard would it be to mark the password with ** (6 star signs) as default?

samdark commented 4 years ago

Related to https://github.com/yiisoft/yii2/issues/16295

dsyabitov commented 3 years ago

Hi! I've started to get the same error 400 - on some of POST requests, made with jquery. My problem was that the code from yii.js ( initCsrfHandler() ), which should initialize csrf headers for all requests was executed later than executed jquery requests.