1
votes

I'm new to AngularJS and Python Tornado and currently working on CSRF/XSRF checking. I've checked that "WebService.py" return "set-cookie" in header while I first time sent a GET request to "test_c" and the cookie do created when I checked in browser. But Tornado keep shows "'_xsrf' argument missing from POST" error when the POST request sent to "test"...

After I check the header of POST request, I found that the xsrf cookie is sent in the header with name as 'cookie' (ex: Cookie:PHPSESSID=xxx; X-Csrftoken=xxx; csrftoken=xxx; _xsrf=xxx). The check_xsrf_cookie function defined in tornado\web.py cannot get the xsrf token correctly as the function tried to get token from either POST's argument, header with name 'X-Xsrftoken' or 'X-Csrftoken'.

Thus, I added some code to check csrf token in 'cookie' in the header as below and it worked as expected... I'm wondering that if I fix this in a right way? Or Tornado/AngularJS already fix this with other functions or I just have to add some arguments to make csrf token sent as Tornado expected?

===========================================
Tornado\Web.py
===========================================
def check_xsrf_cookie(self):
    ###### Added by me #####
    _cookies_dict = {}
    _cookies_header_reformat = re.findall(r'\w+=[\w\d.]+', self.request.headers.get('Cookie'))
    for _cookie in _cookies_header_reformat: 
        key, value = _cookie.split('=', 1)
        _cookies_dict[key] = value*
    #########################

    token = (self.get_argument("_xsrf", None) or
             self.request.headers.get("X-Xsrftoken") or
             self.request.headers.get("X-Csrftoken")
    ###### Added by me #####
             or _cookies_dict['csrftoken'])
    #########################
    if not token:
        raise HTTPError(403, "'_xsrf' argument missing from POST")
    _, token, _ = self._decode_xsrf_token(token)
    _, expected_token, _ = self._get_raw_xsrf_token()

    if not _time_independent_equals(utf8(token), utf8(expected_token)):
        raise HTTPError(403, "XSRF cookie does not match POST argument")

===========================================
WebService.py:
===========================================
class Basic(tornado.web.RequestHandler):
    def set_default_headers(self): 
        self.set_header('Access-Control-Allow-Origin', self.request.headers.get('Origin', '*'))
        self.set_header('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT, OPTIONS')
        self.set_header('Access-Control-Allow-Credentials', 'true')

class test(Basic): 
    def get(self): 
        self.write('hi')

    def put(self): 
        self.set_status(200)

    def post(self): 
        print('ok')

    def delete(self): 
        self.set_status(200)

class test_c(Basic): 
    def get(self): 
        self.set_cookie('_xsrf', '12345')

settings = {
    "xsrf_cookies": True, 
    "debug": True, 
}

application = tornado.web.Application([
    (r"/test", test), 
    (r"/test_c", test_c), 
], **settings)

===========================================
JavaScript.js:
===========================================
(function() {
    angular.module('ngRouteExample', ['ngCookies'])
        .config(function($httpProvider) {
            $httpProvider.defaults.withCredentials = true;
        })
        .controller('MainController', function($http, $scope) {
            $http.get('http://localhost:8889/test_c')
                .success(function(headers, data) {
                    $http.post('http://localhost:8889/test')
                        .then(function() {
                            alert('!');
                        });
                });
        });
}) ();

EDIT: I removed all codes I added to Tornado\web.py. Instead, I returned the cookie's value when "test_c" was called. And set up the header when POST request is issued from JavaScript. But I get "XSRF cookie does not match POST argument" error when POST request verified the toke.

I checked that both tokens returned from GET request, sent from POST request and printed when "test_c" was triggered are all '6e785017a6a1c28377a7d92187806136'.

But when I print "token" and "expected_token" from Tornado\web.py, they turns into different values... "token" was shown as b'nxP\x17\xa6\xa1\xc2\x83w\xa7\xd9!\x87\x80a6' and "expected_token" as b'\x11\xc4/\xa9\xd4\xe3\x83\xa2\xd9`\xc4\x12\xaf2\xfeK'...

===========================================
WebService.py
===========================================
class test_c(Basic): 
    def get(self): 
        if(self.get_cookie('X-Xsrftoken') == None): 
            self.set_cookie('X-Xsrftoken', hashlib.md5(str(time.localtime()).encode('utf8')).hexdigest())
        print(type(self.get_cookie('X-Xsrftoken'))) # For Debug
        print(self.get_cookie('X-Xsrftoken'))       # For Debug
        self.write(self.get_cookie('X-Xsrftoken'))

===========================================
JavaScript.js
===========================================
    .controller('MainController', function($http, $scope) {
        $http.get('http://localhost:8889/test_c')
            .success(function(data) {
                $http.post('http://localhost:8889/test', '1', {headers: {'X-Xsrftoken': data}})
                    .then(function() {
                        alert('!');
                    });
            });
    });

EDIT 2 I dug into Tornado\web.py and found a way to fix the issue I mentioned in last EDIT, but not sure if it's the right way. Please let me know if any other better approach.

In Tornado\web.py, it tried to match CSRF token from POST argument or header with the cookie it stored in "check_xsrf_cookie" function. And Tornado use "_get_raw_xsrf_token" function to get the CSRF cookie with name "_xsrf" but not "X-Xsrftoken" nor "X-Csrftoken" Tornado used to check header. Thus, I modified my "test_c" function to generate the CSRF cookie with name '_xsrf' and returned it to front end. And "JavaScript.js" stay the same way to POST the token in header with name "X-Xsrftoken" so Tornado can retrieve it when validation.

===========================================
WebService.py
===========================================
class test_c(Basic): 
    def get(self): 
        if(self.get_cookie('_xsrf') == None): 
            self.set_cookie('_xsrf', hashlib.md5(str(time.localtime()).encode('utf8')).hexdigest())
        self.write(self.get_cookie('_xsrf').encode('utf8'))

===========================================
JavaScript.js
===========================================
    .controller('MainController', function($http, $scope) {
        $http.get('http://localhost:8889/test_c')
            .success(function(data) {
                $http.post('http://localhost:8889/test', '1', {headers: {'X-Xsrftoken': data}})
                    .then(function() {
                        alert('!');
                    });
            });
    });
1

1 Answers

1
votes

The point of CSRF protection is that the same value is sent in two different ways: once in the cookie (which the browser sends automatically), and once in the request itself (either the body or the HTTP headers). In a CSRF attack the cookies are "write-only": the browser will send them to the server, but the attacker can't tell what they are. This would let the attacker act as the authenticated user, so to prevent this we require that the CSRF token be present in the request (demonstrating that the page making the request has the ability to read the token).

With your changes, the cookie is used on both sides of the comparison, completely defeating the check. Instead, you must change the javascript side to send the CSRF token in an X-Csrftoken HTTP header (or in the POST body if it's form-encoded).