Skip to content
Future Site
All news
Feature

Running other people's templates safely with Twig's sandbox

4 min read

The CVolve Template Maker studio with a Mustache template in the editor, an A4 preview and the design assistant

CVolve lets you import CV templates from almost anywhere: its own .cvolve packages, HTML with Mustache placeholders, Twig, JSON Resume themes written in Handlebars, zip files, and even a finished HTML CV that an AI converts into a template. CVolve Template Maker adds templates generated by AI from a written design brief.

That is a lot of code we did not write, running on the server. This article explains how we keep it safe.

The risk

Twig is a powerful template language. In a normal Symfony app, that power is useful. In a template uploaded by a stranger, it is a risk. A template could try to:

  • call methods on objects passed to it,
  • include other files from the server,
  • use functions and filters that were never meant for templates,
  • print unescaped HTML and inject scripts into a shared CV.

One engine for every format

The first decision was to not run five different template engines. The CVolve template kit converts every supported format into Twig:

  • Mustache HTML ({{header.name}}, {{#items}}…{{/items}}) is converted by a MustacheConverter.
  • Handlebars JSON Resume themes are converted by a HandlebarsConverter, with partials inlined.
  • Static HTML is converted with the help of the user's AI, then checked like everything else.

So there is exactly one place where templates run, and we only need to secure that one place.

The sandbox

That place is TemplateSandbox. It renders each template in its own Twig environment, with Twig's SandboxExtension and an explicit security policy:

$policy = new SecurityPolicy(self::TAGS, self::FILTERS, [], [], self::FUNCTIONS, self::TESTS);
$policy->setStrict(true);
$twig->addExtension(new SandboxExtension($policy, true));

The policy is an allow-list. Anything not on it is rejected:

  • Tags: if, for, set, apply, macro, import, from, with, verbatim. There is no include, extends, embed or use, so templates cannot load other files.
  • Filters: about thirty safe ones such as join, upper, length and escape, plus CVolve's own CV helpers (md, period, pretty_url, initials, as_list).
  • Functions: only range, max, min and cycle.
  • Tests: only simple ones like defined, empty and iterable. Strict mode denies every other test, including constant.
  • Methods and properties: none. The two empty arrays in the policy mean no object method or property may be accessed.

Plain data only

The allow-list for methods is empty, and we go further: templates never receive objects. The renderer turns the CV into plain arrays and strings before rendering. Even if a policy rule were wrong, there would be no object to call a method on.

Output is autoescaped as HTML. A CV that contains <script> in a job title prints it as text, not as code.

Defence in depth

The sandbox is the main protection, but not the only one:

  1. Validation before install. A template is checked against its schema.json and manifest.json: required regions, supported sections, limits and fonts. Template Maker's Problems panel shows exactly these checks.
  2. AI output gets no special trust. When AI generates or edits a template, the result goes through the same converter, sandbox and validator. If it fails, the AI gets one automatic chance to fix it, and every edit in the design assistant is a diff the designer applies or discards.
  3. The browser is locked too. PDFs are rendered by Chromium in Gotenberg, which may only load data: URLs. A template cannot load remote resources even through CSS.

One kit, two apps

All of this lives in a small library, the CVolve template kit. CVolve includes it as a local Composer package, and Template Maker loads the same package from a copy of the CVolve repository:

"repositories": [
    {"type": "path", "url": "../cvolve/packages/template-kit", "options": {"symlink": true}}
]

The result is a simple promise to designers: if a template validates in the Template Maker, it installs and renders the same way in CVolve, because it is literally the same code.

Takeaway

If your users can upload templates, choose a template language with a real sandbox, convert every other format into it, allow-list instead of deny-list, and pass plain data instead of objects. Twig's sandbox makes this practical in PHP, and the extra layers cost very little.

Both projects are MIT licensed. The sandbox is in packages/template-kit/src/Sandbox/ in the CVolve repository.