8000 [HttpKernel] Fix restoring surrogate content from cache · symfony/symfony@d773882 · GitHub
[go: up one dir, main page]

Skip to content

Commit d773882

Browse files
[HttpKernel] Fix restoring surrogate content from cache
1 parent f1122a2 commit d773882

File tree

7 files changed

+74
-33
lines changed

7 files changed

+74
-33
lines changed

src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,15 @@ protected function removeFromControl(Response $response)
133133
$response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value));
134134
}
135135
}
136+
137+
protected static function generateBodyEvalBoundary(): string
138+
{
139+
static $cookie;
140+
$cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
141+
$boundary = base64_encode($cookie);
142+
143+
\assert(\strlen($boundary) === HttpCache::BODY_EVAL_BOUNDARY_LENGTH);
144+
145+
return $boundary;
146+
}
136147
}

src/Symfony/Component/HttpKernel/HttpCache/Esi.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ public function process(Request $request, Response $response)
8080
$content = preg_replace('#<esi\:remove>.*?</esi\:remove>#s', '', $content);
8181
$content = preg_replace('#<esi\:comment[^>]+>#s', '', $content);
8282

83-
static $cookie;
84-
$cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
85-
$boundary = base64_encode($cookie);
83+
$boundary = self::generateBodyEvalBoundary();
8684
$chunks = preg_split('#<esi\:include\s+(.*?)\s*(?:/|</esi\:include)>#', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
8785

8886
$i = 1;

src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
*/
3030
class HttpCache implements HttpKernelInterface, TerminableInterface
3131
{
32+
public const BODY_EVAL_BOUNDARY_LENGTH = 24;
33+
3234
private $kernel;
3335
private $store;
3436
private $request;
@@ -631,26 +633,22 @@ protected function store(Request $request, Response $response)
631633
private function restoreResponseBody(Request $request, Response $response)
632634
{
633635
if ($response->headers->has('X-Body-Eval')) {
634-
ob_start();
636+
\assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24);
635637

636-
if ($response->headers->has('X-Body-File')) {
637-
include $response->headers->get('X-Body-File');
638-
} else {
639-
$content = $response->getContent();
638+
ob_start();
640639

641-
if (substr($content, -24) === $boundary = substr($content, 0, 24)) {
642-
$j = strpos($content, $boundary, 24);
643-
echo substr($content, 24, $j - 24);
644-
$i = $j + 24;
640+
$content = $response->getContent();
641+
$boundary = substr($content, 0, 24);
642+
$j = strpos($content, $boundary, 24);
643+
echo substr($content, 24, $j - 24);
644+
$i = $j + 24;
645645

646-
while (false !== $j = strpos($content, $boundary, $i)) {
647-
[$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4);
648-
$i = $j + 24;
646+
while (false !== $j = strpos($content, $boundary, $i)) {
647+
[$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4);
648+
$i = $j + 24;
649649

650-
echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors);
651-
echo $part;
652-
}
653-
}
650+
echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors);
651+
echo $part;
654652
}
655653

656654
$response->setContent(ob_get_clean());

src/Symfony/Component/HttpKernel/HttpCache/Ssi.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@ public function process(Request $request, Response $response)
6464

6565
// we don't use a proper XML parser here as we can have SSI tags in a plain text response
6666
$content = $response->getContent();
67-
68-
static $cookie;
69-
$cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
70-
$boundary = base64_encode($cookie);
67+
$boundary = self::generateBodyEvalBoundary();
7168
$chunks = preg_split('#<!--\#include\s+(.*?)\s*-->#', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
7269

7370
$i = 1;

src/Symfony/Component/HttpKernel/HttpCache/Store.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,15 +475,25 @@ private function persistResponse(Response $response): array
475475
/**
476476
* Restores a Response from the HTTP headers and body.
477477
*/
478-
private function restoreResponse(array $headers, string $path = null): Response
478+
private function restoreResponse(array $headers, string $path = null): ?Response
479479
{
480480
$status = $headers['X-Status'][0];
481481
unset($headers['X-Status']);
482+
$content = null;
482483

483484
if (null !== $path) {
484485
$headers['X-Body-File'] = [$path];
486+
unset($headers['x-body-file']);
487+
488+
if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) {
489+
$content = file_get_contents($path);
490+
\assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24);
491+
if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) {
492+
return null;
493+
}
494+
}
485495
}
486496

487-
return new Response($path, $status, $headers);
497+
return new Response($content, $status, $headers);
488498
}
489499
}

src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Symfony\Component\HttpKernel\HttpCache\Store;
1919
use Symfony\Component\HttpKernel\HttpKernelInterface;
2020

21-
class HttpCacheTestCase extends TestCase
21+
abstract class HttpCacheTestCase extends TestCase
2222
{
2323
protected $kernel;
2424
protected $cache;

src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public function testRestoresResponseContentFromEntityStoreWithLookup()
200200
{
201201
$this->storeSimpleEntry();
202202
$response = $this->store->lookup($this->request);
203-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->getContent());
203+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->headers->get('X-Body-File'));
204204
}
205205

206206
public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate()
@@ -253,9 +253,9 @@ public function testStoresMultipleResponsesForEachVaryCombination()
253253
$res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']);
254254
$this->store->write($req3, $res3);
255255

256-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent());
257-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent());
258-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent());
256+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File'));
257+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File'));
258+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File'));
259259

260260
$this->assertCount(3, $this->getStoreMetadata($key));
261261
}
@@ -265,17 +265,17 @@ public function testOverwritesNonVaryingResponseWithStore()
265265
$req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']);
266266
$res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']);
267267
$this->store->write($req1, $res1);
268-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent());
268+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File'));
269269

270270
$req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']);
271271
$res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']);
272272
$this->store->write($req2, $res2);
273-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent());
273+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File'));
274274

275275
$req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']);
276276
$res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']);
277277
$key = $this->store->write($req3, $res3);
278-
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent());
278+
$this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File'));
279279

280280
$this->assertCount(2, $this->getStoreMetadata($key));
281281
}
@@ -330,6 +330,33 @@ public function testDoesNotStorePrivateHeaders()
330330
$this->assertNotEmpty($response->headers->getCookies());
331331
}
332332

333+
public function testDiscardsInvalidBodyEval()
334+
{
335+
$request = Request::create('https://example.com/foo');
336+
$response = new Response('foo', 200, ['X-Body-Eval' => 'SSI']);
337+
338+
$this->store->write($request, $response);
339+
$this->assertNull($this->store->lookup($request));
340+
341+
$request = Request::create('https://example.com/foo');
342+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24).'b';
343+
$response = new Response($content, 200, ['X-Body-Eval' => 'SSI']);
344+
345+
$this->store->write($request, $response);
346+
$this->assertNull($this->store->lookup($request));
347+
}
348+
349+
public function testLoadsBodyEval()
350+
{
351+
$request = Request::create('https://example.com/foo');
352+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24);
353+
$response = new Response($content, 200, ['X-Body-Eval' => 'SSI']);
354+
355+
$this->store->write($request, $response);
356+
$response = $this->store->lookup($request);
357+
$this->assertSame($content, $response->getContent());
358+
}
359+
333360
protected function storeSimpleEntry($path = null, $headers = [])
334361
{
335362
if (null === $path) {

0 commit comments

Comments
 (0)
0