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

Unstable behavior w/ AngularJS ui-router AJAX requests and CSRF token validation #7128

Closed nireve closed 9 years ago

nireve commented 9 years ago

In my AngularJS app I use ui-router to load partial page content that contains a GridView with delete button. The content returned are pure html without inline or linked javascript.

With vanilla GridView, the delete button will actually submit a form with a _csrf parameter whose value is retrieved via yii.getCsrfToken(). I rewrote it so that it now submits delete request via AJAX by using angular's $http to make it look like form submission, including sending the correct content-type header and form data. Two approaches were used to retrieve CSRF tokens, neither of which works completely:

What exactly could go wrong here? I'm using DbSession if that helps. Thanks in advance!

UPDATE: It turns out it has to with how angular-ui-router caches the ajax content. So if I send the new CSRF token with content A, delete request is successful. Then content B is loaded (newer token generated), going back to content A then delete will return 400 because A is cached and still has the old token. See comment for one way of solving this issue.

nireve commented 9 years ago

It turns out it has to with how angular-ui-router caches the ajax content. So if I send the new CSRF token with content A, delete request is successful. Then content B is loaded (newer token generated), going back to content A then delete will return 400 because A is cached and still has the old token.

A possible solution is to create an Application Component, and filter all outgoing response and insert javascript to re-assign meta fields with the latest token.


class CSRFReassign extends Component {

    public function init() {

        Event::on(Response::className(), Response::EVENT_AFTER_PREPARE, function ($event) {
            $response = $event->sender;
            $csrf = \Yii::$app->request->csrfToken;

            // only when content is html are csrf tokens re-issued
            if ($response->format == Response::FORMAT_HTML) {
                $response->content .= "<script>$('meta[name=csrf-token]').prop('content', '$csrf');</script>";
            }
        });
    }
}

Enable this component in the config file:

    'bootstrap' => ['log', 'CSRFReassign'],
    'components' => [
        'CSRFReassign' => [
            'class' => '[your namespace]\CSRFReassign'
        ]
    ...
    ]

Then anytime we make the request, use window.yii.getCsrfToken() to retrieve the (latest) token as usual, no matter how many ajax requests have been made.