Skip to content

Make the sandbox a first-class citizen with a dedicated Sandbox class - #4854

Open
fabpot wants to merge 6 commits into
twigphp:3.xfrom
fabpot:sandbox-first-class-citizen
Open

Make the sandbox a first-class citizen with a dedicated Sandbox class#4854
fabpot wants to merge 6 commits into
twigphp:3.xfrom
fabpot:sandbox-first-class-citizen

Conversation

@fabpot

@fabpot fabpot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

I've been thinking about making the sabdbox feature as a first class citizen for years. With all the work that has been done recently on security issues, I spent some time on it again. Here is the result.

The main ideas:

  • Currently, the sandbox is thigtly coupled to the "main" environment: SandboxExtension is registered on the environmen directly, so it instruments all compiled template, and adds runtime checks to all renders, trusted or not. As recommended in the docs, you should have a dedicated environment for sandboxes, different from the main one, but it's not really "enforced" nor natural to do.
  • As a consequence, we store some state via enableSandbox()/disableSandbox() with try/finally patterns scattered across the codebase to support rendering sandboxed and non-sandboxed templates from a environment.
  • When using one environment, a sandboxed template can include anything the loader can load, sees every application global, and inherits all extensions, this is a footgun (again, already not recommended in the docs).
  • There are too maybe "knobs": global mode, enableSandbox(), {% include(..., sandboxed: true) %}, and {% sandbox %}.

The new Twig\Sandbox\Sandbox class renders untrusted templates through a dedicated, always-sandboxed environment crafted by the developer. Taht way, there is no state to toggle and nothing leaks between the main environment and the sandbox, in either direction.

@upsun-dispatch

upsun-dispatch Bot commented Jul 4, 2026

Copy link
Copy Markdown

📋 PR Summary

This incremental change moves the createTemplate() docblock from the concrete Sandbox class into the SandboxInterface contract and declares the method there, so consumers type-hinting the interface for dependency injection can now reach the documented createTemplate() capability.

Changes
Layer / File(s) Summary
sandbox contract
src/Sandbox/SandboxInterface.php Adds the createTemplate(string, ?string): TemplateWrapper method to the interface (with its docblock) and imports Twig\TemplateWrapper.
src/Sandbox/Sandbox.php Removes the now-redundant docblock above the concrete createTemplate() implementation, since it lives on the interface.

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: full · 26 files reviewed · no issues found

Review details

Commit: Commit ac46f0a
Model: claude-opus-4-8
Panel: security · correctness · robustness · design

@fabpot
fabpot force-pushed the sandbox-first-class-citizen branch from ac46f0a to 34a32bb Compare July 5, 2026 06:50

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: incremental · 27 files reviewed · no new issues

Review details

Commit: Commits ac46f0a..34a32bb
Model: claude-opus-4-8
Panel: security · correctness · robustness · design

@fabpot
fabpot force-pushed the sandbox-first-class-citizen branch from 34a32bb to bf97d98 Compare July 8, 2026 19:46

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: incremental · 28 files reviewed · 🔵 2 infos

Review details

Commit: Commits 34a32bb..bf97d98
Model: claude-opus-4-8
Panel: security · correctness · robustness · design

Comment thread src/Sandbox/Sandbox.php
private Environment $env,
SecurityPolicyInterface $policy,
) {
if ($policy instanceof SecurityPolicy && !$policy->isStrict()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Info — Post-construction mutation quietly defeats the documented strict guarantee.

The strict-policy invariant is only checked at construction and only for SecurityPolicy instances. Because the policy object is shared with the caller (and SecurityChecker::setSecurityPolicy() can replace it), calling $policy->setStrict(false) after new Sandbox(...) leaves the sandbox running with non-strict semantics without any error, so it silently diverges between Twig 3.x and 4.0 behavior.

Comment thread src/Sandbox/Sandbox.php
*/
public function stream(string $name, array $context = []): iterable
{
yield from $this->env->load($name)->stream($context);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Info — Deferred vs eager error timing across parallel methods can surprise callers.

stream() and streamBlock() use yield from, making the whole method a generator, so $this->env->load($name) (and any LoaderError for a missing template) is deferred until the returned iterable is first iterated. render()/display() delegate directly and throw eagerly, so stream('missing') that is never iterated raises nothing while the eager methods raise immediately — a behavioral divergence across the sibling render paths.

Comment thread src/Sandbox/Sandbox.php Outdated
Comment thread src/Sandbox/Sandbox.php
}

if ((new \ReflectionMethod($this->policy, 'checkSecurity'))->getNumberOfParameters() >= 4) {
$this->policy->checkSecurity($tags, $filters, $functions, $tests);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this catch the SecurityNotAllowed*Error exceptions (probably catching SecurityError) to set the source context in them ?

Comment thread src/Sandbox/SecurityPolicy.php
Comment thread src/Sandbox/Sandbox.php
@fabpot
fabpot force-pushed the sandbox-first-class-citizen branch from da4cbba to 8aa73b6 Compare July 28, 2026 18:21

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: full · 32 files reviewed · 🟡 2 warnings · 🔵 1 info · 2 still open


🟡 Warning (P1 — real issues) — outside diff range

general · process

This PR does not merge cleanly onto the base branch: CHANGELOG, doc/sandbox.rst, and tests/Extension/CoreTest.php conflict. The branch must be rebased or the conflicts resolved before it can be merged.

Review details

Commit: Commit da4cbba
Model: claude-opus-4-8
Panel: security · correctness · robustness · design

Outstanding from earlier reviews:

  • #3546865692 — src/Sandbox/Sandbox.php:36: Post-construction mutation quietly defeats the documented strict guarantee.
  • #3546865697 — src/Sandbox/Sandbox.php:59: Deferred vs eager error timing across parallel methods can surprise callers.

Comment thread src/Node/Expression/FunctionNode/RenderSandboxedFunction.php Outdated
Comment thread src/Sandbox/SandboxInterface.php

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: full · 32 files reviewed · 🟡 1 warning · 3 still open


🟡 Warning (P1 — real issues) — outside diff range

general · merge-conflict

The branch does not merge cleanly onto the base branch: CHANGELOG, doc/sandbox.rst, and tests/Extension/CoreTest.php have merge conflicts. The merge is blocked until the branch is rebased and the conflicts are resolved.

Review details

Commit: Commit 8aa73b6
Model: claude-opus-4-8
Panel: security · correctness · robustness · design

Outstanding from earlier reviews:

  • #3546865692 — src/Sandbox/Sandbox.php:36: Post-construction mutation quietly defeats the documented strict guarantee.
  • #3546865697 — src/Sandbox/Sandbox.php:59: Deferred vs eager error timing across parallel methods can surprise callers.
  • #3668201835 — src/Sandbox/SandboxInterface.php:53: A documented method is missing from the contract meant for DI, forcing a concrete-class dependency.

@fabpot
fabpot force-pushed the sandbox-first-class-citizen branch from 8aa73b6 to f4cf0c3 Compare July 28, 2026 20:26

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: full · 31 files reviewed · 🟡 1 warning · 3 still open

Outstanding from earlier reviews:

  • #3546865692 — src/Sandbox/Sandbox.php:36: Post-construction mutation quietly defeats the documented strict guarantee.
  • #3546865697 — src/Sandbox/Sandbox.php:59: Deferred vs eager error timing across parallel methods can surprise callers.
  • #3668201835 — src/Sandbox/SandboxInterface.php:53: A documented method is missing from the contract meant for DI, forcing a concrete-class dependency.

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Upsun Dispatch Review: incremental · 31 files reviewed · no new issues · 3 still open

Outstanding from earlier reviews:

  • #3546865692 — src/Sandbox/Sandbox.php:36: Post-construction mutation quietly defeats the documented strict guarantee.
  • #3546865697 — src/Sandbox/Sandbox.php:59: Deferred vs eager error timing across parallel methods can surprise callers.
  • #3668201830 — src/Node/Expression/FunctionNode/RenderSandboxedFunction.php:66: The documented positional render_sandboxed usage is silently ignored and its safety/validation guards never fire.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants