Grav 2.0: Twig in Content and the New Security Sandbox

Why Twig in page content is now gated and sandboxed, how to turn it back on, and the safer path forward

9 mins

If you've moved a site to Grav 2.0 and a bit of {{ ... }} you had sitting in a page suddenly shows up as literal text instead of rendering, this post is for you. We changed how Twig runs inside page content in 2.0, and it's the single most common "wait, why did this stop working?" question we're getting. It's a deliberate change, it's a security fix, and I want to walk through what it does, why we did it, and what your options are.

The short version: Twig inside your Markdown content is now off by default, and even when you turn it on it runs inside a security sandbox. Templates in your theme and plugins are completely unaffected. The full reference lives on the Learn site at Twig in Content.

What Actually Changed

In Grav 1.7, if you put {{ ... }} or {% ... %} into a page's Markdown and that page had process.twig: true in its front matter, Grav ran it. No questions asked. That was convenient, and a lot of people leaned on it for things like embedding a comments widget, pulling in a config value, or looping over a collection right inside a page.

The problem is that page content is user-authored. Anyone with permission to edit a page could write Twig that did far more than print a date. Twig is a full template language with access to PHP objects, and in 1.7 a page editor could reach into Grav's internals, read configuration that held secrets, touch the filesystem, or in the worst cases get code execution on the server. If you let a junior author or a client edit pages, you were effectively handing them keys they didn't know they had.

So in 2.0, two things are true:

  1. Twig in content is gated. It will not run unless a site administrator explicitly enables it.
  2. When it does run, it runs sandboxed. Only a known-safe allow-list of Twig tags, filters, functions, and object methods is permitted. Anything outside that list is blocked.

This applies only to Twig authored inside page content. The .html.twig files in your theme and your plugins are trusted code that you put on disk yourself, so they are never sandboxed and nothing about normal theme or plugin development changes.

Why We Did This

This is a security fix, plain and simple. The technical name for the risk is Server-Side Template Injection, and it's one of the more serious classes of vulnerability a CMS can have. When untrusted input gets evaluated as a template, the person supplying that input can often escalate all the way to running code on your server.

In 1.7, the line between "content" and "code" was blurry. A page body could be code. That meant your security boundary wasn't really "who can log into admin" or "who can deploy a plugin," it was "who can edit any page," and those are very different levels of trust. Multi-author sites, sites with client access, and sites built by agencies for non-technical owners were all exposed to a degree most people never realized.

Grav 2.0 draws a hard line. Content is content. Code is code. Page bodies are treated as untrusted by default, the same way you'd treat a form submission or a URL parameter, and the sandbox makes sure that even when you do allow Twig in content, it can only do safe, read-only things.

Why 2.0 and Not Sooner

I'll be honest: this is a sizable change, and we know it breaks a habit a lot of people have built up over years. We didn't do it lightly, and we couldn't have done it in a 1.x release even if we'd wanted to.

Locking down Twig in content changes behavior on existing sites by definition. Any site relying on {{ ... }} in a page body would have broken on a point release, and breaking people's sites with a minor update is exactly what we promise not to do. A change with this much blast radius belongs in a major version, where you expect to read the upgrade notes, test, and adjust. That's what 2.0 is for. We held this fix until we had a major version to put it in, and now it's in.

Turning Twig in Content Back On

If you genuinely need Twig in your page content, you can switch it on. Head to Configuration → Security in the Admin panel and find the Twig Content section. There are two toggles that matter:

  • Process Enabled is the master switch. While it's off, Twig in content never runs, no matter what an individual page's front matter says. Turn this on first.
  • Editor Enabled controls who can flip the per-page process.twig switch from inside the editor. With it off, only users with the Twig page permission can. With it on, anyone with page-edit access can.

Prefer config files? The same settings live in user/config/security.yaml:

twig_content:
  process_enabled: true
  editor_enabled: true

Even with the master switch on, Twig still only runs on pages that opt in, exactly as in 1.7. The page needs process.twig: true in its front matter:

---
title: My Page
process:
    twig: true
---

After changing any of this, clear the cache with bin/grav cache so pages re-render.

The Sandbox, and How to Customize It

Turning Twig in content on does not turn off the sandbox. They're separate. The gate decides whether content Twig runs at all; the sandbox decides what it's allowed to do once it does.

Inside the sandbox, Grav permits a generous default allow-list of safe Twig: most of the common filters (date, upper, markdown, json_encode and friends), control structures (for, if, set, include), and read-only access to the page, its media, the active user, and similar everyday objects. That covers the large majority of what people actually do with Twig in content.

When the sandbox blocks something, it fails gently rather than throwing an error in your visitor's face:

  • The blocked expression renders as literal text, so you can see exactly what got stopped.
  • A line is written to logs/security.log telling you which tag, filter, function, or method was blocked and how to allow it.
  • If you're logged in as a super administrator, Grav adds an HTML comment near the spot to point you at the log.

So if a page shows raw {{ something() }} after you've enabled Twig in content, that's the sandbox doing its job, and the security log will tell you which member it didn't recognize.

If you've decided a particular function is safe for your authors to use, you can add it to the allow-list yourself in user/config/security.yaml, which merges on top of the defaults:

twig_sandbox:
  allowed_functions:
    - my_safe_function

The same goes for tags, filters, methods, and properties. Plugin developers can register their own safe Twig members through an event so their plugin works in sandboxed content out of the box. The full list of defaults and every customization option is documented on the Learn site at Twig in Content, and the developer side is covered in the Developer Upgrade Guide.

Only ever allow members you're certain are safe to run against content that anyone with page-edit access could write. Adding something to the allow-list is the same trust decision as exposing it in the first place. If a function reads files, evaluates strings, or reaches into Grav's container, leave it off the list. When in doubt, don't.

The Better Path: Get Twig Out of Your Content

Here's my actual recommendation, and it's the same one I'd give even without the security angle: wherever you can, move Twig out of page content entirely. The sandbox exists for the cases where you can't, but most of the time you can, and the result is cleaner anyway.

Twig in content mixes presentation logic into what should be plain writing. It's harder to read, harder for non-technical authors to edit safely, and it ties your content to template internals. There are two better homes for that logic.

Use a page template. If a page needs to do something Twig-y once, give it its own template in your theme and set template: in the front matter. Templates are trusted, unsandboxed, and exactly where display logic belongs. You write the Twig once, in code you control, and the page body goes back to being clean Markdown.

Use a shortcode for repeatable, author-friendly pieces. If the thing you keep reaching for Twig to do is something authors should be able to drop into any page, a custom shortcode is almost always the better answer. Instead of teaching every author a snippet of Twig (and then allow-listing whatever it needs), you give them one short, readable tag.

Here's the difference. Before, you might have written this in a page body and needed Twig-in-content enabled to make it work:

{% set staff = page.collection({'items': {'@page.children': '/team'}}) %}
<div class="team">
{% for person in staff %}
  <span>{{ person.title }}</span>
{% endfor %}
</div>

After, the author writes this instead:

[team folder="/team" /]

The page body stays readable, and there's nothing for the sandbox to block because no Twig runs in the content at all. Better still, you don't have to write a plugin (or any PHP) to get there anymore. Shortcode Core now ships a Shortcode Builder: you define your own shortcodes right in the plugin settings, each one backed by a Twig template or a short inline snippet, and the parameters and content an author passes in are auto-escaped for you. It comes with a couple of ready-made examples to crib from, and the editor works in both the classic admin and Admin Next. The full rundown lives in the Shortcode Core README, and there's a step-by-step walkthrough on the Learn site: Creating a Shortcode. Shortcode Core also ships a large set of ready-made shortcodes if you'd rather not build your own.

Quick Links

I know this one stings a little if you'd built workflows around Twig in content, and I get it. But the trade is a real one: a clear line between content and code, a sandbox that keeps multi-author and client sites safe by default, and a nudge toward patterns that are easier to live with anyway. 2.0 was the right place to make that trade.

— Andy

Grav Premium
Turbo-charge your Grav site - from the creators of Grav