I'm working on a plugin that'll create a private API for content authors to post entries to Craft from desktop-based scripts. These scripts post data to plugin controller actions (anonymous-allowed), where the plugin will first check for valid Craft user account details or a plugin-generated key.

Since I'm still enforcing authentication, what's the best way to disable CSRF checks just for these posts when CSRF is globally enabled?

Thoughts I've had:

  1. Attempt to put the CSRF key and token in the $_POST data as early as possible. (Probably not even possible.)
  2. Have the client (desktop) script grab the CSRF key+token just before posting. (Inconvenient, potentially failure-prone.)
  3. Somehow prevent CSRF protection from being enabled for requests passed to this controller.
up vote 10 down vote accepted

Craft is piggybacking off of Yii's CSRF support for 90% of the work here and they've implemented it at the Request level (specifically onBeginRequest).

That happens way before Craft gets a chance to determine what type of request this is, much less figure out where to route it.

I think your best bet is going to be to put some logic in Craft's general.php config file that specifically check's for PHP's $_GET and $_POST globals for a p=some/path/to/my/plugin/controller and disables the enableCsrfProtection config setting for that request.

return array(
    'enableCsrfProtection' => (!isset($_SERVER['REQUEST_URI']) || $_SERVER['REQUEST_URI'] != '/actions/plugin/controller/action'),
);
  • 1
    Thanks Brad (and Brandon)! That'll work just fine for this particular thingamajig. – Matt Stein Nov 4 '14 at 21:29
  • @BradBell @BrandonKelly I'm trying to do something similar, I have an action that processes Paypal IPN requests, but using {{ actionUrl('plugin/action', {}) }} in my template forces $mustShowScriptName to be set to true in UrlHelper::_getUrl(). This means the global $_GET['p'] is not set, as the URL that is printed includes "index.php" like this http://craft.dev/index.php/actions/plugin/action?param=foo. omitScriptNameInUrls set to true in config seems to have no effect on this. Can you think of what's going wrong? – Mike Pierce Nov 20 '14 at 14:34
  • 1
    @MikePierce Actually it’s just a matter of checking $_REQUEST['PATH_INFO'], not $_GET['p']. Just updated the example code. – Brandon Kelly Nov 21 '14 at 17:31
  • 1
    @taylor Craft's general.php config file. Update the answer. – Brad Bell Dec 17 '15 at 0:17
  • 3
    If you're coming here from craftcommerce.com/docs/payment-gateways the action routes for the payment gateway callbacks that you want to disable csrf for, according to Luke, are actions/commerce/payments/completePayment for all, or actions/commerce/payments/acceptNotification if you use sagepay – billythekid Mar 22 '17 at 15:57

As Brad's answer suggests, you can conditionally enable or disable CSRF production via the General Config, based on any criteria you want.

Some use cases might be:

  • match a SERVER['REQUEST_URI'] string or substring, to open up a particular set of controllers or actions

  • check the $_SERVER['REMOTE_ADDR'], to allow token-less requests from a particular IP or IP range

  • validate 'safe' requests based on a key or encrypted timestamp from some $_POST[] or $_GET[] variable

For cleanliness, I typically try to tuck the checking/validation logic away into its own "handler" class. Then, in the general.php, I set the enableCsrfProduction item based on the result of the check...

(In MyRequestHelper.php, something like...)

<?php
namespace Craft;

class MyRequestHelper
{

    public static function isApiRequest()
    {
        $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        return strpos($path, 'actions/my-api') == 1;
    }

}

(In craft/config/general.php, something like...)

require_once (..path/to/MyRequestHelper.php');
$isApiRequest = \Craft\MyRequestHelper::isApiRequest();

return [

    // ...other configs...

    'enableCsrfProtection' => !$isApiRequest,

];

I modified Michael's answer to add support for query strings + make it nicer to check for multiple routes:

if ( !function_exists('isApiRequest') ) {
    function isApiRequest ()
    {
        $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        $query = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);

        $actions = [
            'actions/charge/webhook/callback',
            'actions/commerce/payments/completePayment',
            'actions/some/other/action',
            'actions/commerce/webhooks/process-webhook'

    ];

    foreach ($actions as $action) {
            if ( strpos($path, $action) || strpos($query, $action) ) {
                return true;
            }
        }

    return false;
    }
}

return [

    // ...other configs...

    'enableCsrfProtection' => !isApiRequest(),

];

In Craft 3 this is a cinch:

class PluginController extends Controller
{
    // Disable CSRF validation for the entire controller 
    public $enableCsrfValidation = false;

    ...

See the Yii2 docs for more control over inividual actions: https://github.com/yiisoft/yii2/blob/master/docs/guide/security-best-practices.md#avoiding-csrf

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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