Search code examples
phpangularjsajaxyiicsrf

Yii 1.1.17: CSRF token validation fails with POST via Angular Controller


I have an Yii application in which I have a link (outside a form) triggering an Angular controller posting some data. The problem is that Yii does not validate the CSRF token when that happens.

My raw url looks like this:

<a id="yt1" href="#" ng-click="markAllAsRead(23, '1eb4e3ac755e22939a0fc8d5ea0e9bacb453319a')" title="Read All" tooltips-size="small" tooltips="1" class="notification-tool ng-isolate-scope"><i class="fa fa-eye"></i></a>

My Angular controller calls an angular service which looks like this:

notificationsService.markAllAsRead = function (user_id, csrf) {
        var dataObject = {
            user_id: user_id,
            YII_CSRF_TOKEN: csrf
        };

        $http.post("/api/notifications/readAll", dataObject).success(function (data) {
            return data;
        });
    };

The POST request looks liek this: POST request via ajax

If I disable the CSRF validation the call succeeds.

Any ideas?

Thanks!

COMPLETE ANSWER:

After some investigation I noticed that the $_POST (also Yii's $request->getPost()) was actually empty, even though Angular was posting data. Reading this answer on stackoverflow it seems that it actually is an issue that has to do with Angular JS and its' default behavior to post as apllication/json (well, maybe not exactly an issue). As suggested by the marked answer to this question and based on the suggestion on the linked answer, I ended up overriding the CHttpRequest class from Yii as follows:

class AppRequest extends CHttpRequest
{
    public function validateCsrfToken($event)
    {
        if ($this->getIsPostRequest() ||
            $this->getIsPutRequest() ||
            $this->getIsPatchRequest() ||
            $this->getIsDeleteRequest()
        ) {
            $cookies = $this->getCookies();

            $method = $this->getRequestType();
            switch ($method) {
                case 'POST':
                    if (empty($this->getPost($this->csrfTokenName))) {
                        $input = json_decode(file_get_contents('php://input'), true);;
                        $userToken = $input[$this->csrfTokenName];
                    } else {
                        $userToken = $this->getPost($this->csrfTokenName);
                    }
                    break;
                case 'PUT':
                    if (empty($this->getPut($this->csrfTokenName))) {
                        $input = json_decode(file_get_contents('php://input'), true);;
                        $userToken = $input[$this->csrfTokenName];
                    } else {
                        $userToken = $this->getPut($this->csrfTokenName);
                    }
                    break;
                case 'PATCH':
                    if (empty($this->getPatch($this->csrfTokenName))) {
                        $input = json_decode(file_get_contents('php://input'), true);;
                        $userToken = $input[$this->csrfTokenName];
                    } else {
                        $userToken = $this->getPatch($this->csrfTokenName);
                    }
                    break;
                case 'DELETE':
                    if (empty($this->getDelete($this->csrfTokenName))) {
                        $input = json_decode(file_get_contents('php://input'), true);;
                        $userToken = $input[$this->csrfTokenName];
                    } else {
                        $userToken = $this->getDelete($this->csrfTokenName);
                    }
                    break;
            }

            if (!empty($userToken) && $cookies->contains($this->csrfTokenName)) {
                $cookieToken = $cookies->itemAt($this->csrfTokenName)->value;
                $valid = $cookieToken === $userToken;
            } else
                $valid = false;
            if (!$valid)
                throw new CHttpException(400, Yii::t('yii', 'The CSRF token could not be verified.'));
        }
    }
}

Solution

  • If you will look at CHttpRequest and watch how validation performed, you will understand problem.

    public function validateCsrfToken($event)
    {
        if ($this->getIsPostRequest() ||
            $this->getIsPutRequest() ||
            $this->getIsPatchRequest() ||
            $this->getIsDeleteRequest())
        {
            $cookies=$this->getCookies();
            $method=$this->getRequestType();
            switch($method)
            {
                case 'POST':
                    $userToken=$this->getPost($this->csrfTokenName);
                break;
                case 'PUT':
                    $userToken=$this->getPut($this->csrfTokenName);
                break;
                case 'PATCH':
                    $userToken=$this->getPatch($this->csrfTokenName);
                break;
                case 'DELETE':
                    $userToken=$this->getDelete($this->csrfTokenName);
            }
            if (!empty($userToken) && $cookies->contains($this->csrfTokenName))
            {
                $cookieToken=$cookies->itemAt($this->csrfTokenName)->value;
                $valid=$cookieToken===$userToken;
            }
            else
                $valid = false;
            if (!$valid)
                throw new CHttpException(400,Yii::t('yii','The CSRF token could not be verified.'));
        }
    }
    

    So to make CSRF validation work we need 2 conditions:

    1. Client must be eligible to send a request with cookie and accept cookies.

    2. We need to pass token with our request.

    2nd condition in your case not working. You'r passing json to request body, and Yii trying to get token from post:

    $userToken=$this->getPost($this->csrfTokenName);
    

    To change this behavior, you need to override CHttpRequest and change config file to use your Request class, e.g.

      'components' => array(
        'request' => array(
            'class' => 'application.components.HttpRequest',
            'enableCsrfValidation' => true,
        ),
      ),
    

    Hope this will help with understanding of what's happening when CSRF validation performed.