11

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.

5 Answers 5

13

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'),
);
8
  • 1
    Thanks Brad (and Brandon)! That'll work just fine for this particular thingamajig.
    – Matt Stein
    Commented Nov 4, 2014 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? Commented Nov 20, 2014 at 14:34
  • 1
    @MikePierce Actually it’s just a matter of checking $_REQUEST['PATH_INFO'], not $_GET['p']. Just updated the example code. Commented Nov 21, 2014 at 17:31
  • 1
    @taylor Craft's general.php config file. Update the answer.
    – Brad Bell
    Commented Dec 17, 2015 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 Commented Mar 22, 2017 at 15:57
23

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

1
  • This is the Craft 3 answer. Thanks, Ben! Commented Sep 13, 2021 at 22:38
11

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,

];
10

UPDATE: This is not necessary to do when using Commerce 2.


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(),

];
3

Try to disable the CSRF protection by event

use Yii;
use yii\base\ActionEvent;
use yii\base\Event;
use yii\web\Controller;

Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, function (ActionEvent $actionEvent) {
    if ($actionEvent->action->id == 'update-cart') {
        Yii::$app->controller->enableCsrfValidation = false;
    }
});

Your Answer

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

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