Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
bug #27309 Fix surrogate not using original request (Toflar)
This PR was submitted for the 3.4 branch but it was squashed and merged into the 2.8 branch instead (closes #27309).

Discussion
----------

Fix surrogate not using original request

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        |

Warning: This might need some close attention. It took me hours to wrap my head around that issue :-)

So the problem is that `HttpCache::forward()` (or essentially any part in your application) can modify the `$request` that is passed to the `HttpCache::handle()` and the surrogate can never access the original request using `HttpCache::getRequest()` anymore.

Example:

* Main request (GET `/foobar`)
* It's not in the cache, so `HttpCache::forward()` modifies `REMOTE_ADDR` to `127.0.0.1` and adds the `X-Forwarded-For` header.
* The request is sent to the application and any e.g. `kernel.request` listener might modify the `$request` further.
* Now the `/foobar` route returns `text/html` that contains some `<esi src="=/fragment_path"` tag.
* `HttpCache` has an instance of `SurrogateInterface` so (in our case `Esi`) will be asked to `process()` and then later on `handle()` the `/fragment_path` request. For that, `Esi` (or in fact `AbstractSurrogate` uses the following line to create a subrequest and pass it on to the application again:

```php
$subRequest = Request::create($uri, Request::METHOD_GET, array(), $cache->getRequest()->cookies->all(), array(), $cache->getRequest()->server->all());
```

What you can see here, is that it uses `$cache->getRequest()`. And here follows the problem:
We did not duplicate (clone) the original request so essentially `$cache->getRequest()` is a reference to the current request that `HttpKernel::forward()` modified and probably any other part of the application did so too. So for example the original `REMOTE_ADDR` (client IP) got lost.

What we should do instead is duplicate the original request so the surrogates can actually behave like a real reverse proxy such as Varnish would by keeping all the original request attributes.

Commits
-------

ab86f43 Fix surrogate not using original request
  • Loading branch information
nicolas-grekas committed Jun 19, 2018
2 parents 9f1d1d8 + ab86f43 commit 3fc2200
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 1 deletion.
6 changes: 5 additions & 1 deletion src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
Expand Up @@ -185,7 +185,11 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
// FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism
if (HttpKernelInterface::MASTER_REQUEST === $type) {
$this->traces = array();
$this->request = $request;
// Keep a clone of the original request for surrogates so they can access it.
// We must clone here to get a separate instance because the application will modify the request during
// the application flow (we know it always does because we do ourselves by setting REMOTE_ADDR to 127.0.0.1
// and adding the X-Forwarded-For header, see HttpCache::forward()).
$this->request = clone $request;
if (null !== $this->surrogate) {
$this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy();
}
Expand Down
39 changes: 39 additions & 0 deletions src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
Expand Up @@ -11,9 +11,12 @@

namespace Symfony\Component\HttpKernel\Tests\HttpCache;

use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
Expand Down Expand Up @@ -1432,6 +1435,42 @@ public function testDoesNotCacheOptionsRequest()
$this->assertHttpKernelIsNotCalled();
$this->assertSame('get', $this->response->getContent());
}

public function testUsesOriginalRequestForSurrogate()
{
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$store = $this->getMockBuilder(StoreInterface::class)->getMock();

$kernel
->expects($this->exactly(2))
->method('handle')
->willReturnCallback(function (Request $request) {
$this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));

return new Response();
});

$cache = new HttpCache($kernel,
$store,
new Esi()
);

$request = Request::create('/');
$request->server->set('REMOTE_ADDR', '10.0.0.1');

// Main request
$cache->handle($request, HttpKernelInterface::MASTER_REQUEST);

// Main request was now modified by HttpCache
// The surrogate will ask for the request using $this->cache->getRequest()
// which MUST return the original request so the surrogate
// can actually behave like a reverse proxy like e.g. Varnish would.
$this->assertSame('10.0.0.1', $cache->getRequest()->getClientIp());
$this->assertSame('10.0.0.1', $cache->getRequest()->server->get('REMOTE_ADDR'));

// Surrogate request
$cache->handle($request, HttpKernelInterface::SUB_REQUEST);
}
}

class TestKernel implements HttpKernelInterface
Expand Down

0 comments on commit 3fc2200

Please sign in to comment.