1

I ran into an issue with CodeIgniter / CSRF / JSON.

I am sending http POST requests to my PHP backend with the Content-Type "application/json. The payload is JSON data. Along with the data, I pass the CSRF token that is generated and stored in the CSRF cookie. With a standard POST FORM request, it works just fine, but when sending as JSON it fails.

As $_POST array is empty because of the JSON content-type, CodeIgniter fails to validate the cookie and throws an error.

How can I have CodeIgniter check JSON payload and validate my CSRF token ?

4 Answers 4

2

To fix that issue, I had to change the code of the "Security.php" file located in "system/core/".

In function "csrf_verify", replace that code:

// Do the tokens exist in both the _POST and _COOKIE arrays?
if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]))
{
$this->csrf_show_error();
}
// Do the tokens match?
if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
{
$this->csrf_show_error();
}

By that code:

// Do the tokens exist in both the _POST and _COOKIE arrays?
if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])) {
    // No token found in $_POST - checking JSON data
    $input_data = json_decode(trim(file_get_contents('php://input')), true); 
    if ((!$input_data || !isset($input_data[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])))
        $this->csrf_show_error(); // Nothing found
    else {
        // Do the tokens match?
        if ($input_data[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
            $this->csrf_show_error();
    }
}
else {
    // Do the tokens match?
    if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
        $this->csrf_show_error();
}

That code first checks $_POST then if nothing has been found, it checks the JSON payload.

The ideal way of doing this would be to check the incoming request Content-Type header value. But surprisingly, it's not straight forward to do ...

If someone has a better solution, please post it here.

Cheers

Sign up to request clarification or add additional context in comments.

1 Comment

It would be best to do this by extending the Security library, rather than modifying the core file directly in system/core. Create My_Security.php in application/core and override csrf_verify(). More about how to do this here: ellislab.com/codeigniter/user-guide/general/core_classes.html
2

Alternatively, you can skip the CSRF checking by adding following code on application/config/config.php below Line No. 351 (based on CI 2.1.4).

 $config['csrf_expire'] = 7200; // This is line no. 351

/* If the REQUEST_URI has method is POST and requesting the API url,
    then skip CSRF check, otherwise don't do. */
if (isset($_SERVER["REQUEST_URI"]) &&
   (isset($_SERVER['REQUEST_METHOD']) && ($_SERVER['REQUEST_METHOD'] == 'POST') ))
{
    if (stripos($_SERVER["REQUEST_URI"],'/api/') === false )  { // Verify if POST Request is not for API
        $config['csrf_protection'] = TRUE;
    }
    else {
        $config['csrf_protection'] = FALSE;
    }
} else {
    $config['csrf_protection'] = TRUE;
}

Comments

0

If this needs to be overridden, best to extend the Security library rather than editing the core file directly.

Create the file My_Security.php in application/core/ and add the following (from the solution above):

<?php
class My_Security extends CI_Security {
    /**
     * Verify Cross Site Request Forgery Protection
     *
     * @return  object
     */
    public function csrf_verify()
    {
        // If it's not a POST request we will set the CSRF cookie
        if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
        {
            return $this->csrf_set_cookie();
        }

        // Do the tokens exist in both the _POST and _COOKIE arrays?
        if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])) {
            // No token found in $_POST - checking JSON data
            $input_data = json_decode(trim(file_get_contents('php://input')), true);
            if ((!$input_data || !isset($input_data[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])))
                $this->csrf_show_error(); // Nothing found
            else {
                // Do the tokens match?
                if ($input_data[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
                    $this->csrf_show_error();
            }
        }
        else {
            // Do the tokens match?
            if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
                $this->csrf_show_error();
        }        // We kill this since we're done and we don't want to

        // polute the _POST array
        unset($_POST[$this->_csrf_token_name]);

        // Nothing should last forever
        unset($_COOKIE[$this->_csrf_cookie_name]);
        $this->_csrf_set_hash();
        $this->csrf_set_cookie();

        log_message('debug', 'CSRF token verified');

        return $this;
    }

}

Comments

0

As Brian write, you have to put your custom class into /application/core/ ex. My_Security.php

This is mine solution, work for me, i check the application/json content_type and request cookies.

defined('BASEPATH') OR exit('No direct script access allowed');

class MY_Security extends  CI_Security {


    public function csrf_verify()
    {

        // If it's not a POST request we will set the CSRF cookie
        if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
        {
            return $this->csrf_set_cookie();
        }

                /**
                 *  mine implementation for application/json
                 */
                $reqHeaders = getallheaders();
                $content_type = $reqHeaders["Content-Type"];

                #it's a json request?
                if(preg_match("/(application\/json)/i",$content_type))
                {
                    #the check the cookie from request
                    $reqCookies = explode("; ",$reqHeaders["Cookie"]);
                    foreach($reqCookies as $c)
                    {
                        if(preg_match("/(".$this->_csrf_cookie_name."\=)/", $c))
                        {
                          $c = explode("=",$c);

                          if($_COOKIE[$this->_csrf_cookie_name] == $c[1])
                          {
                             return $this; 
                          }
                        }
                    }

                }
                //< end

        // Check if URI has been whitelisted from CSRF checks
        if ($exclude_uris = config_item('csrf_exclude_uris'))
        {
            $uri = load_class('URI', 'core');
            foreach ($exclude_uris as $excluded)
            {
                if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string()))
                {
                    return $this;
                }
            }
        }

        // Do the tokens exist in both the _POST and _COOKIE arrays?
        if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])
            OR $_POST[$this->_csrf_token_name] !== $_COOKIE[$this->_csrf_cookie_name]) // Do the tokens match?
        {
            $this->csrf_show_error();
        }

        // We kill this since we're done and we don't want to polute the _POST array
        unset($_POST[$this->_csrf_token_name]);

        // Regenerate on every submission?
        if (config_item('csrf_regenerate'))
        {
            // Nothing should last forever
            unset($_COOKIE[$this->_csrf_cookie_name]);
            $this->_csrf_hash = NULL;
        }

        $this->_csrf_set_hash();
        $this->csrf_set_cookie();

        log_message('info', 'CSRF token verified');
        return $this;
    }

}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.