4
\$\begingroup\$

I'm building a website where all data interactions are done over ajax other than the initial page load. Now I discovered the need for CSRF tokens.

I elected to deliver the token in a meta tag like so:

<meta name="csrf" content="JI8qMif9gHuD06m/HByCzg==" />

The function that generates the token is as so:

public function csrf()
{
    return str_replace('+','',base64_encode(openssl_random_pseudo_bytes(16)));
}

All controllers for ajax extend a parent controller called JsonController. The JSON controller handles the check of the CSRF token in its constructor (I'll explain why later):

function __construct()
{
    $app = \StarterKit\App::getInstance();

    //ensure request is ajax
    if(!$app->slim->request->isXhr()){
        $app->halt(403,'Access Denied to Ajax Resource');
    }
    //validate csrf token, then update.
    if($app->slim->request->isPost()){
        $fail = false;
        if(!isset($app->post['csrf'])){
            $fail = true;
        }else{
            if($app->post['csrf'] !== $app->session['csrf']){
                $fail = true;
            }
        }
        if($fail !== false){
            $app->halt(403,'Access Denied');
        }
    }

    if($app->slim->request->isGet()){
        $fail = false;
        if(!isset($app->get['csrf'])){
            $fail = true;
        }else{
            if($app->get['csrf'] !== $app->session['csrf']){
                $fail = true;
            }
        }
        if($fail !== false){
            $app->halt(403,'Access Denied');
        }
    }
    $this->app = $app;
}

Now, for the part where I am still unsure of. How should I return the new CSRF token to the client securely? Json controller has a method called __try() this method's responsibility is to invoke the particular Ajax route (method), capture its response (or an exception) and return a JSON encoded response. It is here in this function that the CSRF token is updated. I assume it is safe to return the CSRF token to the user in the JSON response here?

public function __try($method)
{
    try{
        if(!method_exists($this,$method)){
            throw new \exception('method not found');
        }
        $msg = call_user_func([$this,$method]);
    }
    catch(\exception $e){
        if($this->app->debug === true){
            $err = $e->getMessage().'|'.$e->getFile().'|'.$e->getLine();
        }else{
            $err = $e->getMessage();
        }
        $msg = ['error'=>1,'message'=>$err];
    }
    $msg['csrf'] = $this->app->session['csrf'] = $this->app->csrf();
    echo json_encode($msg, JSON_HEX_QUOT | JSON_HEX_TAG);
}

The JavaScript component of the equation here is insignificant. It basically gets/sets the CSRF token and injects it into the request body of any Ajax request (I've built an API wrapper around $.ajax so this is an automated process).

I've tested it and it works, but I'm dying to know: is this even secure? It seems like it is to me because I tried copying the session ID into Firefox and using it to access the session with no success. I'm curious to know your thoughts.

\$\endgroup\$
3
\$\begingroup\$

Yes, you seem to have it correct.

openssl_random_pseudo_bytes(16) is good because it is generating a 64 bit token value for the token, which is more than strong enough.

The

    //ensure request is ajax
    if(!$app->slim->request->isXhr()){
        $app->halt(403,'Access Denied to Ajax Resource');
    }

check is great because this inherently protects against CSRF. This would be checking a header such as X-Requested-With which cannot be sent cross domain without CORS. Therefore it makes it impossible for an attacker to send such a request cross domain for their malicious site.

Remember there is no need to refresh the CSRF token, apart from once per session. It can sometimes hurt usability if you do because it can stop your application working if there are multiple tabs open.

I assume it is safe to return the CSRF token to the user in the JSON response here?

Yes, because an attacker will not be able to read the response. Of course implementing SSL/TLS so your URLs are encrypted over HTTPS is highly recommended to prevent any Man-In-The-Middle or eavesdropping attacks.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.