Error: The action you requested is not allowed. My API is blocked by filters when the HTTP request method is "POST" in CodeIgniter 4
Asked Answered
D

2

1

I am building a Web App along with API for Android using CI4.

For the Web App, I have a filter to check whether the user already logged in with some exceptions, one of them is to ignore the filter if the URL consists api/* (The url for API is http://localip/api/)

The API is working fine if the request method is GET. I can get the data from API. But when I tried to insert a data to database using POST method, it redirects me to login page (I'm using Postman to test the API)

How do I fix this?

What I have tried so far was adding login filter alias to

public $methods = [
        'post' => ['csrf', 'loginfilter']
    ]; But still not working

Here is the full code

Filters.php

<?php

namespace Config;

use App\Filters\CorsFilter;
use App\Filters\LoginFilter;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\SecureHeaders;

class Filters extends BaseConfig
{
    /**
     * Configures aliases for Filter classes to
     * make reading things nicer and simpler.
     *
     * @var array
     */
    public $aliases = [
        'loginfilter' => LoginFilter::class,
        'cors' => CorsFilter::class
    ];

    /**
     * List of filter aliases that are always
     * applied before and after every request.
     *
     * @var array
     */
    public $globals = [
        'before' => [
            // 'honeypot',
            'csrf',
            'loginfilter' => ['except' => ['/', '/login', 'api/*']],
            'cors'
            // 'invalidchars',
        ],
        'after' => [
            'toolbar',
            // 'honeypot',
            // 'secureheaders',
        ],
    ];

    /**
     * List of filter aliases that works on a
     * particular HTTP method (GET, POST, etc.).
     *
     * Example:
     * 'post' => ['csrf', 'throttle']
     *
     * @var array
     */
    public $methods = [
        'post' => ['csrf','loginfilter]
    ];

    /**
     * List of filter aliases that should run on any
     * before or after URI patterns.
     *
     * Example:
     * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
     *
     * @var array
     */
    public $filters = [];
}

LoginFilter.php

<?php

namespace App\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;

class LoginFilter implements FilterInterface
{

    public function before(RequestInterface $request, $arguments = null)
    {
        $session = session();
        if (!$session->has('user_id')) {
            return redirect()->to(base_url() . '/');
        }
    }

    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
    }
}

Routes.php

$routes->resource("api/role", ['controller' => 'apis\MasterDataRoleApi']);

MasterDataRoleApi.php (Controller)

<?php

namespace App\Controllers\apis;

use App\Models\GeneralModels;
use App\Models\RoleModel;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\RESTful\ResourceController;

class MasterDataRoleApi extends ResourceController
{
    use ResponseTrait;
    protected $model;
    protected $generalModel;

    public function __construct()
    {
        $this->model = new RoleModel();
        $this->generalModel = new GeneralModels();
    }

    public function index()
    {
        $role= $this->request->getVar('role');
        $data = $this->model->getRoleApi($role);
        return $this->respond($data, 200);
    }

    public function create()
    {
        $roleName = $this->request->getPost('role_name');
        $supervisor = $this->request->getPost('supervisor');
        $userId = $this->request->getVar("userId");
        helper('idgenerator');
        $maxCode = $this->generalModel->getMaxData('tmrole', 'role_id');
        $generatedId = idGenerator($maxCode[0]['role_id'], 4, 3, "JAB-");

        $this->model->insertTmRole($generatedId, $roleName, $userId, $userId);
       

        $data = array();

        $dataArr = array(
            "response" => "Success",
            "response_details" => "Saved Successfully"
        );

        $data[] =  $dataArr;

        return $this->respondCreated($data, 201);
    }
}

Image below shows Json returned when request method is GET https://i.sstatic.net/2ZKPd.png

Image below shows login page returned when request method is POST https://i.sstatic.net/11R0z.png

Thank you in advance.

Dimaggio answered 16/3, 2022 at 11:4 Comment(2)
hang on, the loginfilter job is to redirect non-logged in user, right? on $global you've set loginfilter to exclude api/* ,correct? now, i do wonder, why on earth loginfilter has to run on $methods->post? wont that make every post fails if you're not logged in?Chippy
Even if loginfilters is not included in $methods->post, it still doesn't work. By default, $methods->post only contain csrf and yet the API doesn't work.Dimaggio
V
0

PART A: CSRF

Explanation 1:

Image below shows Json returned when request method is GET https://i.sstatic.net/2ZKPd.png

Your API GET requests work just fine because they're not protected.

Cross-site request forgery (CSRF)

The CSRF Protection is only available for POST/PUT/PATCH/DELETE requests. Requests for other methods are not protected.

Explanation 2:

Image below shows login page returned when request method is POST https://i.sstatic.net/11R0z.png

Error

The action you requested is not allowed.

That error comes from system/Security/Security.php::verify()

throw SecurityException::forDisallowedAction();

You normally receive this error in 2 scenarios:

  1. When you forgot to submit the CSRF token along with your request (POST/PUT/etc.).
  2. When the CSRF token submitted along with your HTML form/request body doesn't match the one that exists in the CSRF cookie.

After having turned on the CSRF filter, when you make an HTTP request and are missing a CSRF cookie, one is auto-generated for you and sent in the HTTP Set-Cookie: '...' response header.

In case you're using the default Cookie-based CSRF Protection, with any further (POST/PUT/etc.) requests made, you will be expected to submit a matching CSRF token along with your request.

Double Submit Cookie

This technique is easy to implement and is stateless. In this technique, we send a random value in both a cookie and as a request parameter, with the server verifying if the cookie value and request value match. When a user visits (even before authenticating to prevent login CSRF), the site should generate a (cryptographically strong) pseudorandom value and set it as a cookie on the user's machine separate from the session identifier. The site then requires that every transaction request include this pseudorandom value as a hidden form value (or other request parameter/header). If both of them match at server side, the server accepts it as legitimate request and if they don't, it would reject the request.

CSRF Solution:

To pass the Double Submit Cookie test, every time you make a (POST/PUT/etc.) request, you will need to submit a CSRF cookie and a request parameter/header having a matching CSRF token. I.e:

Requirement 1: Cookie: csrf_cookie_name=ccd8facfa8229bdba5e0160c108d1a02; HTTP request header.

Requirement 2:

The Order of Token Sent by Users

The order of checking the availability of the CSRF token is as follows:

  1. $_POST array (I.e: <input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />).
  2. HTTP header (I.e: X-CSRF-TOKEN: ccd8facfa8229bdba5e0160c108d1a02).
  3. php://input (JSON request) - bear in mind that this approach is the slowest one since we have to decode JSON and then re-encode it. (I.e: {"csrf_cookie_name": "ccd8facfa8229bdba5e0160c108d1a02"})

Requirement 1 is normally auto-fulfilled by the browser if that's where you're initiating the request from. Nonetheless, you can manually fulfil it. I.e: In your PHP main view template, you can use Ajax:

<script>
        $.ajaxSetup({
            headers: { 
                "<?php echo csrf_header();?>": "<?php echo csrf_hash();?>",

                "Cookie": "<?php echo csrf_token();?>"="<?php echo csrf_hash();?>"; "<?php echo session_name();?>":"<?php echo session_id();?>"
             }
        });

<script>


Below are my personal .env file configurations that worked for me.

Change myapp.local to your application's domain.

.env file.

app.baseURL = 'http://myapp.local'
app.sessionExpiration = 86400

cookie.domain = '.myapp.local'
cookie.httponly = false

security.expires = 86400
security.regenerate = false

The rest of the .env file configurations related to SECURITY/CSRF, COOKIE, APP/SESSION were maintained in their default setting.

As I conclude the CSRF section, if you still have issues, install & configure Xdebug, add break points inside this method system/Security/Security.php::verify() and confirm if your HTTP request is passing the conditions in there. Make sure your Xdebug configurations are similar to: (xdebug.log_level = 0 | xdebug.mode = debug,develop | xdebug.start_with_request = yes | xdebug.client_port = 9004 | xdebug.client_host = "localhost" | xdebug.trace_output_name = "trace.%c.%t-%s.%H_%R" | xdebug.profiler_output_name = "cachegrind.out.%t-%s.%H_%R" | xdebug.remote_handler = "dbgp" | xdebug.show_local_vars = 9)

PART B: Postman OR Insomnia

This section covers setting up your REST API client application tool to be able to test your project's end-points.

HTTP Request Header Requirements:

  1. X-Requested-With: XMLHttpRequest
  2. X-CSRF-TOKEN: 62b04a891414ef789bee7108f94ad97a
  3. Content-Type: application/x-www-form-urlencoded
  4. Accept: */*
  5. Cookie: csrf_cookie_name=62b04a891414ef789bee7108f94ad97a; ci_session=4ji7amn186ckbo0gutdoe3ai6ufumk4e
  6. User-Agent: insomnia/2022.1.1
  7. Host: myapp.local

The above headers are mandatory. You may add other HTTP request headers after the ones listed above.

The User-Agent can vary depending on if you are using one tool or another (I.e Insomnia or Postman).

The Host can vary as well, depending on how you set up your application's base URL.

The Content-Type can vary, depending on the kind of data you're submitting to the server. (I.e. HTML FORM -> application/x-www-form-urlencoded and JSON -> application/json).

As for the Cookie header, you may need to attach the ci_session (session_id) cookie if in case your endpoint is behind an Authentication system or Login filter. Of course, this may not be necessary if your API uses a different Auth mechanism (i.e: using Bearer/Access tokens with an extra Authorization: Bearer xxx_my_bearer_token_here header). In addition, you may need to attach a csrf_cookie_name cookie as shown above if your application has enabled the csrf filter. In this case, you may want to create a dedicated GET API route endpoint specifically to allow you receive a csrf cookie name and value (hash). Alternatively, if you're too lazy to set up a dedicated endpoint for sending out CSRF tokens and you already have the web app running, open your browser's console while logging in/navigating through your app, in the Application TAB -> Cookies -> your domain, you should be able to see and utilize these cookies in Postman / Insomnia.

enter image description here

The X-CSRF-TOKEN HTTP request header value should be the same as the csrf_cookie_name cookie value.

HTTP Request Call Requirements:

I.e:

POST http://myapp.local/api/companies

Request Body. Don't forget to add the csrf_cookie_name token as part of the request body.

Key Value
company_name Tesla, Inc.
csrf_cookie_name 62b04a891414ef789bee7108f94ad97a

As I conclude with PART B:

Important items Description
ci_session cookie or an Authorization: Bearer xxxxxxx HTTP request header. Allows you to authenticate with your application/project only for Auth protected API endpoints. In your particular case, I believe your loginfilter is working with a ci_session cookie and the cookie is expected to be sent along with every request with the help of the Cookie HTTP request header.
csrf_cookie_name cookie and (a X-CSRF-TOKEN HTTP request header or CSRF token request parameter). The CSRF cookie and (X-CSRF-TOKEN HTTP request header or CSRF token request parameter) values MUST match. This is a requirement if you've turned on the csrf filter.
Vestpocket answered 18/3, 2022 at 11:18 Comment(0)
A
0

From what I can see is, you have loginfilter as a fallback for every POST method. That might be the culprit over there.

That being said, here is an alternate solution. You could group the routes in routes.php and apply loginfilter to those routes. Additionally you can nest them and partition them as you want.

Example :

$routes->group(
    'api',
    ['filter' => 'loginfilter'],
    function ($routes) {
        $routes->resource("role", ['controller' => 'apis\MasterDataRoleApi']);
    }
);

You can remove the global filters while using this method.

Aggy answered 18/3, 2022 at 5:49 Comment(0)
V
0

PART A: CSRF

Explanation 1:

Image below shows Json returned when request method is GET https://i.sstatic.net/2ZKPd.png

Your API GET requests work just fine because they're not protected.

Cross-site request forgery (CSRF)

The CSRF Protection is only available for POST/PUT/PATCH/DELETE requests. Requests for other methods are not protected.

Explanation 2:

Image below shows login page returned when request method is POST https://i.sstatic.net/11R0z.png

Error

The action you requested is not allowed.

That error comes from system/Security/Security.php::verify()

throw SecurityException::forDisallowedAction();

You normally receive this error in 2 scenarios:

  1. When you forgot to submit the CSRF token along with your request (POST/PUT/etc.).
  2. When the CSRF token submitted along with your HTML form/request body doesn't match the one that exists in the CSRF cookie.

After having turned on the CSRF filter, when you make an HTTP request and are missing a CSRF cookie, one is auto-generated for you and sent in the HTTP Set-Cookie: '...' response header.

In case you're using the default Cookie-based CSRF Protection, with any further (POST/PUT/etc.) requests made, you will be expected to submit a matching CSRF token along with your request.

Double Submit Cookie

This technique is easy to implement and is stateless. In this technique, we send a random value in both a cookie and as a request parameter, with the server verifying if the cookie value and request value match. When a user visits (even before authenticating to prevent login CSRF), the site should generate a (cryptographically strong) pseudorandom value and set it as a cookie on the user's machine separate from the session identifier. The site then requires that every transaction request include this pseudorandom value as a hidden form value (or other request parameter/header). If both of them match at server side, the server accepts it as legitimate request and if they don't, it would reject the request.

CSRF Solution:

To pass the Double Submit Cookie test, every time you make a (POST/PUT/etc.) request, you will need to submit a CSRF cookie and a request parameter/header having a matching CSRF token. I.e:

Requirement 1: Cookie: csrf_cookie_name=ccd8facfa8229bdba5e0160c108d1a02; HTTP request header.

Requirement 2:

The Order of Token Sent by Users

The order of checking the availability of the CSRF token is as follows:

  1. $_POST array (I.e: <input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />).
  2. HTTP header (I.e: X-CSRF-TOKEN: ccd8facfa8229bdba5e0160c108d1a02).
  3. php://input (JSON request) - bear in mind that this approach is the slowest one since we have to decode JSON and then re-encode it. (I.e: {"csrf_cookie_name": "ccd8facfa8229bdba5e0160c108d1a02"})

Requirement 1 is normally auto-fulfilled by the browser if that's where you're initiating the request from. Nonetheless, you can manually fulfil it. I.e: In your PHP main view template, you can use Ajax:

<script>
        $.ajaxSetup({
            headers: { 
                "<?php echo csrf_header();?>": "<?php echo csrf_hash();?>",

                "Cookie": "<?php echo csrf_token();?>"="<?php echo csrf_hash();?>"; "<?php echo session_name();?>":"<?php echo session_id();?>"
             }
        });

<script>


Below are my personal .env file configurations that worked for me.

Change myapp.local to your application's domain.

.env file.

app.baseURL = 'http://myapp.local'
app.sessionExpiration = 86400

cookie.domain = '.myapp.local'
cookie.httponly = false

security.expires = 86400
security.regenerate = false

The rest of the .env file configurations related to SECURITY/CSRF, COOKIE, APP/SESSION were maintained in their default setting.

As I conclude the CSRF section, if you still have issues, install & configure Xdebug, add break points inside this method system/Security/Security.php::verify() and confirm if your HTTP request is passing the conditions in there. Make sure your Xdebug configurations are similar to: (xdebug.log_level = 0 | xdebug.mode = debug,develop | xdebug.start_with_request = yes | xdebug.client_port = 9004 | xdebug.client_host = "localhost" | xdebug.trace_output_name = "trace.%c.%t-%s.%H_%R" | xdebug.profiler_output_name = "cachegrind.out.%t-%s.%H_%R" | xdebug.remote_handler = "dbgp" | xdebug.show_local_vars = 9)

PART B: Postman OR Insomnia

This section covers setting up your REST API client application tool to be able to test your project's end-points.

HTTP Request Header Requirements:

  1. X-Requested-With: XMLHttpRequest
  2. X-CSRF-TOKEN: 62b04a891414ef789bee7108f94ad97a
  3. Content-Type: application/x-www-form-urlencoded
  4. Accept: */*
  5. Cookie: csrf_cookie_name=62b04a891414ef789bee7108f94ad97a; ci_session=4ji7amn186ckbo0gutdoe3ai6ufumk4e
  6. User-Agent: insomnia/2022.1.1
  7. Host: myapp.local

The above headers are mandatory. You may add other HTTP request headers after the ones listed above.

The User-Agent can vary depending on if you are using one tool or another (I.e Insomnia or Postman).

The Host can vary as well, depending on how you set up your application's base URL.

The Content-Type can vary, depending on the kind of data you're submitting to the server. (I.e. HTML FORM -> application/x-www-form-urlencoded and JSON -> application/json).

As for the Cookie header, you may need to attach the ci_session (session_id) cookie if in case your endpoint is behind an Authentication system or Login filter. Of course, this may not be necessary if your API uses a different Auth mechanism (i.e: using Bearer/Access tokens with an extra Authorization: Bearer xxx_my_bearer_token_here header). In addition, you may need to attach a csrf_cookie_name cookie as shown above if your application has enabled the csrf filter. In this case, you may want to create a dedicated GET API route endpoint specifically to allow you receive a csrf cookie name and value (hash). Alternatively, if you're too lazy to set up a dedicated endpoint for sending out CSRF tokens and you already have the web app running, open your browser's console while logging in/navigating through your app, in the Application TAB -> Cookies -> your domain, you should be able to see and utilize these cookies in Postman / Insomnia.

enter image description here

The X-CSRF-TOKEN HTTP request header value should be the same as the csrf_cookie_name cookie value.

HTTP Request Call Requirements:

I.e:

POST http://myapp.local/api/companies

Request Body. Don't forget to add the csrf_cookie_name token as part of the request body.

Key Value
company_name Tesla, Inc.
csrf_cookie_name 62b04a891414ef789bee7108f94ad97a

As I conclude with PART B:

Important items Description
ci_session cookie or an Authorization: Bearer xxxxxxx HTTP request header. Allows you to authenticate with your application/project only for Auth protected API endpoints. In your particular case, I believe your loginfilter is working with a ci_session cookie and the cookie is expected to be sent along with every request with the help of the Cookie HTTP request header.
csrf_cookie_name cookie and (a X-CSRF-TOKEN HTTP request header or CSRF token request parameter). The CSRF cookie and (X-CSRF-TOKEN HTTP request header or CSRF token request parameter) values MUST match. This is a requirement if you've turned on the csrf filter.
Vestpocket answered 18/3, 2022 at 11:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.