Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Twig Sandbox Bundle Laravel Package

intaro/twig-sandbox-bundle

Symfony bundle integrating Twig's sandbox extension to safely render untrusted templates with configurable security policies, allowed tags, filters, functions, and methods. Ideal for user-supplied templates.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register the bundle (there is no Flex recipe, so this is manual):

composer require intaro/twig-sandbox-bundle:^3.4
// config/bundles.php
Intaro\TwigSandboxBundle\IntaroTwigSandboxBundle::class => ['all' => true],

Point it at the code it should scan and give it a cache location:

# config/packages/intaro_twig_sandbox.yaml
intaro_twig_sandbox:
    cache_dir: '%kernel.cache_dir%/twig_sandbox'
    paths:
        - '%kernel.project_dir%/src/Entity'
        - '%kernel.project_dir%/src/ViewModel'

Confirm the option names against src/DependencyInjection/Configuration.php in the version you install — the README is terse and the bundle has churned config shape across releases.

Turn on strict mode (v3.4.0+, PR #27). This is the headline of the new release: the validator can now fail loudly instead of skipping/warning. Expose it per-environment rather than hardcoding it:

# config/packages/intaro_twig_sandbox.yaml
intaro_twig_sandbox:
    strict: '%kernel.debug%'   # verify the key name in Configuration.php for your version

Warm the cache once with it on before you rely on it — see the Gotchas section for what "strict" actually catches in your version.

Annotate one class to allow read-only access:

namespace App\Entity;

use Intaro\TwigSandboxBundle\Annotation\SecurityPolicy;

#[SecurityPolicy(methods: ['getTitle', 'isPublished', 'getAuthor'], properties: ['id'])]
class Article
{
    // ...
}

If your version predates attribute support, fall back to the Doctrine annotation form (@SecurityPolicy(methods={"getTitle"}, properties={"id"})) and keep doctrine/annotations installed.

Enable the sandbox itself. The bundle only builds the policy; Twig\Extension\SandboxExtension still has to be registered and the sandbox turned on at render time (via {% sandbox %} or enableSandbox()).

First use case: user-editable templates — notification/email/PDF bodies stored in the DB and edited by non-developers — rendered against a small set of entities or view models. Smoke-test it end to end:

$twig->createTemplate('{{ article.title }}')->render(['article' => $article]); // should work
$twig->createTemplate('{{ article.delete() }}')->render(['article' => $article]); // must throw SecurityNotAllowedMethodError

Where to look first in the repo: the annotation/attribute class, DependencyInjection/Configuration.php (what is actually configurable), the compiler pass / cache warmer that does the scanning, and — new in 3.4.0 — whatever the strict-mode flag gates in the validator. Those few files are essentially the whole bundle.

Implementation Patterns

Annotate view models, not Doctrine entities. The policy is keyed by get_class($object), and a lazy-loaded entity is a Proxies\__CG__\App\Entity\Article — a class name that does not exist in your scan paths. Either map to a DTO before handing it to the sandbox, or force initialization and normalize with Doctrine\Common\Util\ClassUtils::getClass() in a policy decorator. DTOs also let you drop setters entirely, which is the real win: the allowlist becomes the contract.

Only ever allow accessors, never mutators or magic. The sandbox checks method names, so a leaked setPassword() or __call() passthrough is a live injection surface. Keep the annotation list explicit and treat any methods entry containing set, save, delete, or __ as a code smell.

Run strict mode in CI, loose mode in dev. Whatever the flag catches — unannotated classes inside your scan paths, or annotations pointing at methods/properties that no longer exist — those are exactly the failures you want at build time, not at the first user render. Concretely: keep strict: false locally while you are iterating on annotations, and run a container build / cache:warmup with strict: true (e.g. an APP_STRICT_SANDBOX=1 env in your CI job and prod deploy) so a renamed accessor breaks the pipeline rather than a customer's template. If strict mode is always on in your version and produces noisy false positives on classes you intentionally leave unannotated, narrow paths rather than turning the feature off.

Let the bundle own methods/properties; own functions/filters/tags yourself. The annotation layer covers the per-class dimension. Functions (dump, source, template_from_string), filters (raw), and tags (include, embed, extends) are global policy and are usually configured once on the SecurityPolicy service. Keep that list in a single file so you can audit it in one diff.

Isolate the rendering path in a dedicated Twig environment. Don't render user templates through the same twig service that renders your admin UI — a missing enableSandbox() call then becomes a privilege escalation. Register a user_twig environment (own loader restricted to your user-template directory, sandbox extension always on, strict_variables: false) and make it the only way user content is rendered:

$sandbox = $twig->getExtension(SandboxExtension::class);
$sandbox->enableSandbox();
try {
    return $twig->createTemplate($source)->render($context);
} finally {
    $sandbox->disableSandbox();
}

Two-tier templates. Trusted templates use {% sandbox %}{% include 'user/' ~ name %}{% end sandbox %} so the {% sandbox %} tag lives in developer-controlled code and the untrusted fragment is the only thing subject to the policy.

Treat the annotation cache as a build artifact. The policy is written into cache_dir at warmup. Run bin/console cache:warmup in your build/deploy step, and make sure the directory exists and is writable at build time if production runs on a read-only filesystem. This is also the natural place to enforce strict mode — a failed warmup fails the deploy.

Test the negative cases, not just the happy path. A small data-provider test asserting SecurityNotAllowedMethodError / SecurityNotAllowedPropertyError for article.delete(), article.__construct(), and article.privateField is the cheapest regression net you'll get, and it catches annotation typos that would otherwise silently widen the policy. Pair it with a warmup-under-strict-mode test so the two failure classes (too permissive vs. stale annotation) are both covered.

Runtime-varying policies are out of scope. Annotations are static per class. If different tenants get different field visibility, keep the annotations as the ceiling and decorate the generated SecurityPolicyInterface with a role-aware wrapper that intersects against the static list.

Gotchas and Tips

  • Strict mode's exact semantics are not documented in the release notes. PR #27 is a one-line changelog entry; before you depend on it, read the diff and confirm (a) the config key, (b) whether it throws at warmup or at first validation, (c) whether it applies to "class in scan path with no annotation" or to "annotation references a missing method", or both, and (d) whether it is opt-in or becomes the default. Upgrade cost is otherwise low — it's a minor bump with a single PR, no breaking-change notice.
  • Properties are checked case-sensitively; methods are not. Twig lowercases method names before comparison but does an in_array() on properties. A typo in properties: ['Id'] fails silently at policy-build time and blows up only when a user renders the template. Strict mode should be the thing that surfaces this — verify it actually does, because if it doesn't, this pitfall is unchanged.
  • Method resolution beats property resolution. Twig tries getTitle/isTitle/hasTitle before falling back to a public property. If your class has getTitle(), allowing properties: ['title'] does nothing — the method check fires first and throws. Allow the accessor names, not the property names.
  • Array and ArrayAccess access behaves differently from object access. Test {{ foo['bar'] }} and {% for k, v in map %} explicitly; the sandbox check for non-objects is a frequent surprise and a frequent source of "it works in dev, not in prod."
  • {% sandbox %} is a trusted-template tag. If you put it inside an untrusted template, the sandboxed environment will reject it (it isn't in allowedTags) — that's correct behaviour, but the error message is opaque.
  • Errors leak class names and method names back to whoever typed the template. Catch Twig\Sandbox\SecurityError at the boundary and return a generic "template references a disallowed field" message. Note that strict mode may introduce its own exception type at build time — make sure it is not the same class you are swallowing at render time, or you'll mask real policy failures.
  • Extension points for vendor classes are limited. You cannot annotate \DateTime or a third-party model that lives outside your scan paths. Check for an extra-classes/registry config key; if there isn't one, a thin SecurityPolicyInterface decorator that merges in extra allowed methods is the way out. If strict mode complains about such classes, this decorator is also where you silence it legitimately.
  • Dev cache staleness. In dev, container rebuilds may not be triggered by annotation changes if the bundle doesn't register the scanned directories as container resources. When a new annotation "doesn't work," run cache:clear before debugging anything else — and if strict mode reports an annotation that you have already fixed, suspect the cache before the code.
  • The sandbox is not a resource-exhaustion boundary. It blocks methods and properties; it does nothing about infinite {% for %} loops, deeply nested includes, or multi-megabyte templates. Add iteration caps and a template size limit at the application layer, and disable Twig's auto_reload for the user environment in production.
  • Maturity risk is real and cheap to hedge. Seven stars, zero dependents, one maintainer, no Flex recipe — but the surface area is a single annotation class plus a scanner plus (now) a strict-mode switch. Pin the version in composer.json (^3.4 at minimum, or an exact tag), and be prepared to vendor or fork it. If you have fewer than ~10 classes to allowlist, a hand-written SecurityPolicy built in services.yaml is less machinery than a bundle and has no upgrade story to worry about.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource