From: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Mon, 16 Mar 2015 15:12:02 +0100
Subject: Safe escaping of fragments for eval()

https://github.com/symfony/symfony/commit/195c57e1f50765aff33137689b16e126a689056a
---
 src/Symfony/Component/HttpKernel/HttpCache/Esi.php | 62 +++++++++++-----------
 .../HttpKernel/Tests/HttpCache/EsiTest.php         |  4 +-
 2 files changed, 33 insertions(+), 33 deletions(-)

diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
index 9dd99d6..ad63267 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
@@ -29,6 +29,10 @@ use Symfony\Component\HttpKernel\HttpKernelInterface;
 class Esi
 {
     private $contentTypes;
+    private $phpEscapeMap = array(
+        array('<?', '<%', '<s', '<S'),
+        array('<?php echo "<?"; ?>', '<?php echo "<%"; ?>', '<?php echo "<s"; ?>', '<?php echo "<S"; ?>'),
+    );
 
     /**
      * Constructor.
@@ -158,10 +162,34 @@ class Esi
 
         // we don't use a proper XML parser here as we can have ESI tags in a plain text response
         $content = $response->getContent();
-        $content = str_replace(array('<?', '<%'), array('<?php echo "<?"; ?>', '<?php echo "<%"; ?>'), $content);
-        $content = preg_replace_callback('#<esi\:include\s+(.*?)\s*(?:/|</esi\:include)>#', array($this, 'handleEsiIncludeTag'), $content);
-        $content = preg_replace('#<esi\:comment[^>]*(?:/|</esi\:comment)>#', '', $content);
         $content = preg_replace('#<esi\:remove>.*?</esi\:remove>#', '', $content);
+        $content = preg_replace('#<esi\:comment[^>]*(?:/|</esi\:comment)>#', '', $content);
+
+        $chunks = preg_split('#<esi\:include\s+(.*?)\s*(?:/|</esi\:include)>#', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
+        $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]);
+
+        $i = 1;
+        while (isset($chunks[$i])) {
+            $options = array();
+            preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, PREG_SET_ORDER);
+            foreach ($matches as $set) {
+                $options[$set[1]] = $set[2];
+            }
+
+            if (!isset($options['src'])) {
+                throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.');
+            }
+
+            $chunks[$i] = sprintf('<?php echo $this->esi->handle($this, %s, %s, %s) ?>'."\n",
+                var_export($options['src'], true),
+                var_export(isset($options['alt']) ? $options['alt'] : '', true),
+                isset($options['onerror']) && 'continue' == $options['onerror'] ? 'true' : 'false'
+            );
+            ++$i;
+            $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]);
+            ++$i;
+        }
+        $content = implode('', $chunks);
 
         $response->setContent($content);
         $response->headers->set('X-Body-Eval', 'ESI');
@@ -214,32 +242,4 @@ class Esi
             }
         }
     }
-
-    /**
-     * Handles an ESI include tag (called internally).
-     *
-     * @param array $attributes An array containing the attributes.
-     *
-     * @return string The response content for the include.
-     *
-     * @throws \RuntimeException
-     */
-    private function handleEsiIncludeTag($attributes)
-    {
-        $options = array();
-        preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $attributes[1], $matches, PREG_SET_ORDER);
-        foreach ($matches as $set) {
-            $options[$set[1]] = $set[2];
-        }
-
-        if (!isset($options['src'])) {
-            throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.');
-        }
-
-        return sprintf('<?php echo $this->esi->handle($this, %s, %s, %s) ?>'."\n",
-            var_export($options['src'], true),
-            var_export(isset($options['alt']) ? $options['alt'] : '', true),
-            isset($options['onerror']) && 'continue' == $options['onerror'] ? 'true' : 'false'
-        );
-    }
 }
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php
index 23e256e..8ab3d47 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php
@@ -131,10 +131,10 @@ class EsiTest extends \PHPUnit_Framework_TestCase
         $esi = new Esi();
 
         $request = Request::create('/');
-        $response = new Response('foo <?php die("foo"); ?><%= "lala" %>');
+        $response = new Response('<?php <? <% <script language=php>');
         $esi->process($request, $response);
 
-        $this->assertEquals('foo <?php echo "<?"; ?>php die("foo"); ?><?php echo "<%"; ?>= "lala" %>', $response->getContent());
+        $this->assertEquals('<?php echo "<?"; ?>php <?php echo "<?"; ?> <?php echo "<%"; ?> <?php echo "<s"; ?>cript language=php>', $response->getContent());
     }
 
     /**
