question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Template fragment caching potentially harmful

See original GitHub issue

Issue Summary

Django provides optional template fragment caching wherein a portion of a template is cached when it is first rendered and that cache is reused to render that portion of the page until the cache expires or is invalidated.

Wagtail provides two ways of viewing pages that have unpublished content: “Preview” and “View Draft.”

Used in conjunction, these features can create scenarios in which live data may be cached and then rendered in preview OR in which draft data may be cached and rendered on the live site. The former is annoying, the latter is a potential vulnerability that could inadvertently leak unpublished information 😱

Since this behavior exists because all the parts are behaving as designed, it’s not 100% clear to me what the solution should be. I can imagine a few possible solutions off the top of my head:

  1. Add a note to the documentation somewhere cautioning against the use of template fragment caching for this reason
  2. Somehow add Wagtail-specific behavior to the Django {% cache %} tag that causes it to no-op in draft and preview modes (may be possible through middleware or the cache API? not sure)
  3. Create a Wagtail-specific template fragment caching system that respects draft and preview modes

Steps to Reproduce

Start with a Wagtail project with the following files:

models.py

from django.db import models

from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel


class HomePage(Page):
    a_charfield = models.CharField(max_length=255)
    
    content_panels = Page.content_panels + [
        FieldPanel('a_charfield')
    ]

templates/home/home_page.html

{% load cache %}

{% cache 3600 homepage_charfield %}
  {{ self.a_charfield }}
{% endcache %}

(Generate and run all migrations, create necessary admin user, etc.)

From the Wagtail admin, edit and publish a new Home Page:

screen shot 2019-02-12 at 09 52 22

DO NOT visit this page. Edit the page again with new values:

screen shot 2019-02-12 at 09 56 09

Do not save, but click “Preview.” You’ll see a previewed page that says “Privileged Information.” Now, load up the live page. Observe that it also renders “Privileged Information” even though that is unsaved content that an editor was merely testing.

screen shot 2019-02-12 at 09 57 51

  • I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: Yes

Technical details

Tested on:

  • Python version: Python 3.7.2
  • Django version: Django 2.1.7
  • Wagtail version: Wagtail 2.4

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:2
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

5reactions
solarissmokecommented, Nov 27, 2019

For anyone who wants to implement a workaround until a solution is found in core, I’ve done this:

import django
from django.templatetags.cache import CacheNode


class SmartCacheNode(CacheNode):
    """
    A subclass of django.templatetags.cache.CacheNode for Django's default cache tag
    that bypasses the cache entirely if there is a request in the context and
    request.is_preview is True.
    """
    def render(self, context):
        request = context.get('request', None)
        # If request.is_preview, then skip caching entirely
        if request and getattr(request, 'is_preview', False):
            return self.nodelist.render(context)

        return super().render(context)


# Monkey-patch Django's cache node with out own.
django.templatetags.cache.CacheNode = SmartCacheNode

This (a) doesn’t deal with caching outside of templates and (b) will not work for template fragments that don’t have request in the context - so it only partially mitigates the security risk posed by this bug.

1reaction
jorenhamcommented, Dec 6, 2019

Similarly to @solarissmoke 's solution, this is the modified version of the jinja2 cache tag from django-jinja .

from django.core.cache import cache
from django.utils.encoding import force_text
from django_jinja.builtins.extensions import make_template_fragment_key
from jinja2 import TemplateSyntaxError
from jinja2 import nodes
from jinja2.ext import Extension


class CacheExtension(Extension):
    tags = {'cache'}

    def parse(self, parser):
        lineno = next(parser.stream).lineno

        expire_time = parser.parse_expression()
        fragment_name = parser.parse_expression()
        vary_on = []

        while not parser.stream.current.test('block_end'):
            vary_on.append(parser.parse_expression())

        body = parser.parse_statements(['name:endcache'], drop_needle=True)

        return nodes.CallBlock(
            self.call_method('_cache_support',
                             [expire_time, fragment_name,
                              nodes.List(vary_on), nodes.Const(lineno),
                              nodes.ContextReference()]),
            [], [], body).set_lineno(lineno)

    def _cache_support(self, expire_time, fragm_name, vary_on, lineno, context, caller):
        try:
            expire_time = int(expire_time)
        except (ValueError, TypeError):
            raise TemplateSyntaxError('"%s" tag got a non-integer timeout '
                'value: %r' % (list(self.tags)[0], expire_time), lineno)

        cache_key = make_template_fragment_key(fragm_name, vary_on)

        request = context.get('request', None)
        # If request.is_preview, then skip caching entirely
        if request and getattr(request, 'is_preview', False):
            return caller()

        value = cache.get(cache_key)
        if value is None:
            value = caller()
            cache.set(cache_key, force_text(value), expire_time)
        else:
            value = force_text(value)

        return value

Read more comments on GitHub >

github_iconTop Results From Across the Web

How can I cache a wagtail page? - Stack Overflow
Since Wagtail often automates the view for you, the easiest method I've found is to cache at the template layer:.
Read more >
Speed up your website with the new fragment cache (t-cache)
Wouldn't it be marvelous to avoid rendering the same page fragments over and over? Do we really need to read the entire website...
Read more >
Django's cache framework
Template fragment caching ​​ If you're after even more control, you can also cache template fragments using the cache template tag. To give...
Read more >
Think Before You Cache - Medium
The problem with techniques like fragment caching is that they're too easy, and too flexible. You can have caches within caches within caches, ......
Read more >
How to Add Template Caching - Learn Wagtail
In this lesson we're going to take a look at database queries and template fragment caching to speed up our load times (page...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found