diff --git a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php
index f2d809e8de97d..e1d73dc74827d 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php
@@ -133,4 +133,15 @@ protected function removeFromControl(Response $response)
$response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value));
}
}
+
+ protected static function generateBodyEvalBoundary(): string
+ {
+ static $cookie;
+ $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
+ $boundary = base64_encode($cookie);
+
+ \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === \strlen($boundary));
+
+ return $boundary;
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
index 4d86508a42092..9f453249325b2 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
@@ -80,9 +80,7 @@ public function process(Request $request, Response $response)
$content = preg_replace('#.*?#s', '', $content);
$content = preg_replace('#]+>#s', '', $content);
- static $cookie;
- $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
- $boundary = base64_encode($cookie);
+ $boundary = self::generateBodyEvalBoundary();
$chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
$i = 1;
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index 063d4105b160b..b01bd722607a9 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -29,6 +29,8 @@
*/
class HttpCache implements HttpKernelInterface, TerminableInterface
{
+ public const BODY_EVAL_BOUNDARY_LENGTH = 24;
+
private $kernel;
private $store;
private $request;
@@ -631,26 +633,22 @@ protected function store(Request $request, Response $response)
private function restoreResponseBody(Request $request, Response $response)
{
if ($response->headers->has('X-Body-Eval')) {
- ob_start();
+ \assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24);
- if ($response->headers->has('X-Body-File')) {
- include $response->headers->get('X-Body-File');
- } else {
- $content = $response->getContent();
+ ob_start();
- if (substr($content, -24) === $boundary = substr($content, 0, 24)) {
- $j = strpos($content, $boundary, 24);
- echo substr($content, 24, $j - 24);
- $i = $j + 24;
+ $content = $response->getContent();
+ $boundary = substr($content, 0, 24);
+ $j = strpos($content, $boundary, 24);
+ echo substr($content, 24, $j - 24);
+ $i = $j + 24;
- while (false !== $j = strpos($content, $boundary, $i)) {
- [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4);
- $i = $j + 24;
+ while (false !== $j = strpos($content, $boundary, $i)) {
+ [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4);
+ $i = $j + 24;
- echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors);
- echo $part;
- }
- }
+ echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors);
+ echo $part;
}
$response->setContent(ob_get_clean());
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php
index bb48238ff1f4b..61909100e6157 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php
@@ -64,10 +64,7 @@ public function process(Request $request, Response $response)
// we don't use a proper XML parser here as we can have SSI tags in a plain text response
$content = $response->getContent();
-
- static $cookie;
- $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
- $boundary = base64_encode($cookie);
+ $boundary = self::generateBodyEvalBoundary();
$chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
$i = 1;
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
index 5db94f73d68c2..9d7f3e4f6949d 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
@@ -475,15 +475,25 @@ private function persistResponse(Response $response): array
/**
* Restores a Response from the HTTP headers and body.
*/
- private function restoreResponse(array $headers, string $path = null): Response
+ private function restoreResponse(array $headers, string $path = null): ?Response
{
$status = $headers['X-Status'][0];
unset($headers['X-Status']);
+ $content = null;
if (null !== $path) {
$headers['X-Body-File'] = [$path];
+ unset($headers['x-body-file']);
+
+ if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) {
+ $content = file_get_contents($path);
+ \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24);
+ if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) {
+ return null;
+ }
+ }
}
- return new Response($path, $status, $headers);
+ return new Response($content, $status, $headers);
}
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
index e47631d1780ea..c8b48ff811c76 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
@@ -18,7 +18,7 @@
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpKernelInterface;
-class HttpCacheTestCase extends TestCase
+abstract class HttpCacheTestCase extends TestCase
{
protected $kernel;
protected $cache;
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
index 239361bc8c337..aff5329cc96f8 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
@@ -200,7 +200,7 @@ public function testRestoresResponseContentFromEntityStoreWithLookup()
{
$this->storeSimpleEntry();
$response = $this->store->lookup($this->request);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->headers->get('X-Body-File'));
}
public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate()
@@ -253,9 +253,9 @@ public function testStoresMultipleResponsesForEachVaryCombination()
$res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']);
$this->store->write($req3, $res3);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent());
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent());
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File'));
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File'));
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File'));
$this->assertCount(3, $this->getStoreMetadata($key));
}
@@ -265,17 +265,17 @@ public function testOverwritesNonVaryingResponseWithStore()
$req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']);
$res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']);
$this->store->write($req1, $res1);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File'));
$req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']);
$res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']);
$this->store->write($req2, $res2);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File'));
$req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']);
$res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']);
$key = $this->store->write($req3, $res3);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File'));
$this->assertCount(2, $this->getStoreMetadata($key));
}
@@ -330,6 +330,33 @@ public function testDoesNotStorePrivateHeaders()
$this->assertNotEmpty($response->headers->getCookies());
}
+ public function testDiscardsInvalidBodyEval()
+ {
+ $request = Request::create('https://example.com/foo');
+ $response = new Response('foo', 200, ['X-Body-Eval' => 'SSI']);
+
+ $this->store->write($request, $response);
+ $this->assertNull($this->store->lookup($request));
+
+ $request = Request::create('https://example.com/foo');
+ $content = str_repeat('a', 24).'b'.str_repeat('a', 24).'b';
+ $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']);
+
+ $this->store->write($request, $response);
+ $this->assertNull($this->store->lookup($request));
+ }
+
+ public function testLoadsBodyEval()
+ {
+ $request = Request::create('https://example.com/foo');
+ $content = str_repeat('a', 24).'b'.str_repeat('a', 24);
+ $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']);
+
+ $this->store->write($request, $response);
+ $response = $this->store->lookup($request);
+ $this->assertSame($content, $response->getContent());
+ }
+
protected function storeSimpleEntry($path = null, $headers = [])
{
if (null === $path) {