Craft CMS Stack Exchange is a question and answer site for administrators, end users, developers and designers for Craft CMS. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

We have a Craft site in which we include a partial that includes in several different places. CSRF is enabled, and we're appending {{ getCsrfInput() }} to the form.

Because of the way we've set up the site, and are including this partial in so many places, nearly all of the forms we're rendering are using cached CSRF tokens, which invalidates them.

Is there a way to prevent Craft's {% cache %} tag from including the generated CSRF token without ending the cache and starting it up again each time? (eg, {% endcache %}{{ getCsrfInpt() }}{% cache %})

I worry doing so would be risky, because we may include this partial in a part of the site that is not cached, so there'd be no cache tag to end.

Thanks!

Update: I failed to mention that our site works right now without Javascript, so I'd like to avoid it. Otherwise, Lindsey's answer is perfect.

share|improve this question
up vote 7 down vote accepted

You can do something like this:

// First, set all your cached content to a variable:
{% set myContent %}
  {% cache %}
    {# YOUR CACHED CONTENT #}
    %%MY_CSRF%%
  {% endcache %}
{% endset %}

// Then, run a replace on your cached content to insert the new CSRF...
{{ myContent|replace({'%%MY_CSRF%%: getCsrfInput()}) %}
share|improve this answer
    
Also, courtesy hat tip to this post for the general idea of the second solution: craftcms.stackexchange.com/questions/4782/… – Ben Wilkins Feb 23 at 20:51
    
Removed the first solution since it wouldn't work. But the |replace tag method would definitely work. – Brandon Kelly Feb 24 at 0:42

The trick is to inject the CSRF field using JavaScript.

  1. Omit the standard getCsrfInput Twig tag.
  2. Give your form(s) the class of csrf.
  3. Add this Twig code outside of the cached area:
{% includejs %}
    $(function() {
        $('form.csrf').prepend('<input type="hidden" name="{{ craft.config.get('csrfTokenName') }}" value="{{ craft.request.getCsrfToken }}" />');
    });
{% endincludejs %}

This will dynamically inject the correct CSRF input field into every form with the csrf class.


* Disclaimer: This example assumes you're using jQuery. A similar solution can be enacted without jQuery, but some adjustments would need to be made.

share|improve this answer
1  
Elegant. I like this JS solution myself because you can also use upstream caching like nginx or varnish without affecting your security (and trying to whitelist a bunch of pages from caching that use a form for example). – RitterKnight Feb 24 at 1:43

Thanks Lindsey and Ben!

I was hoping to avoid using Javascript to handle CSRF, because our site otherwise is not dependent on it. I took Ben's suggestion of creating a variable outside of the cache, and using it within the cache. I ran a quick test to verify that it indeed would use the uncached value.

I wound up making a Twig extension to get uncached CSRF input, which we can set in our _layout.html:

{# in craft/templates/_layout.html #}
{% set csrfToken = craft.request.getCsrfToken() %}

Then we build the twig extension to access the csrfToken from the Twig context, and render a hidden input element:

// craft/plugins/ourplugin/twigextensions/OurPluginTwigExtension.php

public function getFunctions() {
    return [
        'getUncachedCsrfInput' => new \Twig_Function_Method($this, 'getUncachedCsrfInput', ['needs_context' => true])
    ];
}

public function getUncachedCsrfInput($context) {
    if (craft()->config->get('enableCsrfProtection') === true) {
        $charset = craft()->templates->getTwig()->getCharset();
        //access the csrfToken variable from the Twig context,
        //    or fall back on craft()->request otherwise
        $csrf = array_key_exists('csrfToken', $context) ? $context['csrfToken'] : craft()->request->getCsrfToken();

        return new \Twig_Markup('<input type="hidden" name="'.craft()->config->get('csrfTokenName').'" value="'. $csrf .'">', $charset);
    }
    return '';
}

Then, we can get an uncached <input /> element in all of our forms using:

{# any form #}
<form action="" method="POST">
    ...
    {{ getUncachedCsrfInput }}
    ...
</form>

Thanks for your help!

share|improve this answer
    
If {{ getUncachedCsrfInput() }} is happening inside a {% cache %} tag, this won't work since the {% cache %} tag will cache its entire output and use that cached output on subsequent requests (and each user session gets its own CSRF token value). Also not sure what the point of all this added complexity over just using getCsrfInput(). – Brandon Kelly Feb 24 at 0:44

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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