Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / Tests / Core / EventSubscriber / RedirectResponseSubscriberTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\EventSubscriber;
4
5 use Drupal\Core\EventSubscriber\RedirectResponseSubscriber;
6 use Drupal\Core\Routing\TrustedRedirectResponse;
7 use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
8 use Drupal\Tests\UnitTestCase;
9 use Symfony\Component\DependencyInjection\Container;
10 use Symfony\Component\EventDispatcher\EventDispatcher;
11 use Symfony\Component\HttpFoundation\RedirectResponse;
12 use Symfony\Component\HttpFoundation\Request;
13 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
14 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
15 use Symfony\Component\HttpKernel\HttpKernelInterface;
16 use Symfony\Component\HttpKernel\KernelEvents;
17
18 /**
19  * @coversDefaultClass \Drupal\Core\EventSubscriber\RedirectResponseSubscriber
20  * @group EventSubscriber
21  */
22 class RedirectResponseSubscriberTest extends UnitTestCase {
23
24   /**
25    * The mocked request context.
26    *
27    * @var \Drupal\Core\Routing\RequestContext|\PHPUnit_Framework_MockObject_MockObject
28    */
29   protected $requestContext;
30
31   /**
32    * The mocked request context.
33    *
34    * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface|\PHPUnit_Framework_MockObject_MockObject
35    */
36   protected $urlAssembler;
37
38   /**
39    * {@inheritdoc}
40    */
41   protected function setUp() {
42     parent::setUp();
43
44     $this->requestContext = $this->getMockBuilder('Drupal\Core\Routing\RequestContext')
45       ->disableOriginalConstructor()
46       ->getMock();
47     $this->requestContext->expects($this->any())
48       ->method('getCompleteBaseUrl')
49       ->willReturn('http://example.com/drupal');
50
51     $this->urlAssembler = $this->getMock(UnroutedUrlAssemblerInterface::class);
52     $this->urlAssembler
53       ->expects($this->any())
54       ->method('assemble')
55       ->willReturnMap([
56         ['base:test', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/test'],
57         ['base:example.com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/example.com'],
58         ['base:example:com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/example:com'],
59         ['base:javascript:alert(0)', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/javascript:alert(0)'],
60       ]);
61
62     $container = new Container();
63     $container->set('router.request_context', $this->requestContext);
64     \Drupal::setContainer($container);
65   }
66
67   /**
68    * Test destination detection and redirection.
69    *
70    * @param \Symfony\Component\HttpFoundation\Request $request
71    *   The request object with destination query set.
72    * @param string|bool $expected
73    *   The expected target URL or FALSE.
74    *
75    * @covers ::checkRedirectUrl
76    * @dataProvider providerTestDestinationRedirect
77    */
78   public function testDestinationRedirect(Request $request, $expected) {
79     $dispatcher = new EventDispatcher();
80     $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
81     $response = new RedirectResponse('http://example.com/drupal');
82     $request->headers->set('HOST', 'example.com');
83
84     $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
85     $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
86     $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
87     $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
88
89     $target_url = $event->getResponse()->getTargetUrl();
90     if ($expected) {
91       $this->assertEquals($expected, $target_url);
92     }
93     else {
94       $this->assertEquals('http://example.com/drupal', $target_url);
95     }
96   }
97
98   /**
99    * Data provider for testDestinationRedirect().
100    *
101    * @see \Drupal\Tests\Core\EventSubscriber\RedirectResponseSubscriberTest::testDestinationRedirect()
102    */
103   public static function providerTestDestinationRedirect() {
104     return [
105       [new Request(), FALSE],
106       [new Request(['destination' => 'test']), 'http://example.com/drupal/test'],
107       [new Request(['destination' => '/drupal/test']), 'http://example.com/drupal/test'],
108       [new Request(['destination' => 'example.com']), 'http://example.com/drupal/example.com'],
109       [new Request(['destination' => 'example:com']), 'http://example.com/drupal/example:com'],
110       [new Request(['destination' => 'javascript:alert(0)']), 'http://example.com/drupal/javascript:alert(0)'],
111       [new Request(['destination' => 'http://example.com/drupal/']), 'http://example.com/drupal/'],
112       [new Request(['destination' => 'http://example.com/drupal/test']), 'http://example.com/drupal/test'],
113     ];
114   }
115
116   /**
117    * @dataProvider providerTestDestinationRedirectToExternalUrl
118    */
119   public function testDestinationRedirectToExternalUrl($request, $expected) {
120     $dispatcher = new EventDispatcher();
121     $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
122     $response = new RedirectResponse('http://other-example.com');
123
124     $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
125     $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
126     $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
127     $this->setExpectedException(\PHPUnit_Framework_Error::class);
128     $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
129   }
130
131   /**
132    * @covers ::checkRedirectUrl
133    */
134   public function testRedirectWithOptInExternalUrl() {
135     $dispatcher = new EventDispatcher();
136     $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
137     $response = new TrustedRedirectResponse('http://external-url.com');
138     $request = Request::create('');
139     $request->headers->set('HOST', 'example.com');
140
141     $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
142     $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
143     $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
144     $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
145
146     $target_url = $event->getResponse()->getTargetUrl();
147     $this->assertEquals('http://external-url.com', $target_url);
148   }
149
150   /**
151    * Data provider for testDestinationRedirectToExternalUrl().
152    */
153   public function providerTestDestinationRedirectToExternalUrl() {
154     return [
155       'absolute external url' => [new Request(['destination' => 'http://example.com']), 'http://example.com'],
156       'absolute external url with folder' => [new Request(['destination' => 'http://example.com/foobar']), 'http://example.com/foobar'],
157       'absolute external url with folder2' => [new Request(['destination' => 'http://example.ca/drupal']), 'http://example.ca/drupal'],
158       'path without drupal basepath' => [new Request(['destination' => '/test']), 'http://example.com/test'],
159       'path with URL' => [new Request(['destination' => '/example.com']), 'http://example.com/example.com'],
160       'path with URL and two slashes' => [new Request(['destination' => '//example.com']), 'http://example.com//example.com'],
161     ];
162   }
163
164   /**
165    * @dataProvider providerTestDestinationRedirectWithInvalidUrl
166    */
167   public function testDestinationRedirectWithInvalidUrl(Request $request) {
168     $dispatcher = new EventDispatcher();
169     $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
170     $response = new RedirectResponse('http://example.com/drupal');
171
172     $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
173     $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
174     $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
175     $this->setExpectedException(\PHPUnit_Framework_Error::class);
176     $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
177   }
178
179   /**
180    * Data provider for testDestinationRedirectWithInvalidUrl().
181    */
182   public function providerTestDestinationRedirectWithInvalidUrl() {
183     $data = [];
184     $data[] = [new Request(['destination' => '//example:com'])];
185     $data[] = [new Request(['destination' => '//example:com/test'])];
186     $data['absolute external url'] = [new Request(['destination' => 'http://example.com'])];
187     $data['absolute external url with folder'] = [new Request(['destination' => 'http://example.ca/drupal'])];
188     $data['path without drupal basepath'] = [new Request(['destination' => '/test'])];
189     $data['path with URL'] = [new Request(['destination' => '/example.com'])];
190     $data['path with URL and two slashes'] = [new Request(['destination' => '//example.com'])];
191
192     return $data;
193   }
194
195   /**
196    * Tests that $_GET only contain internal URLs.
197    *
198    * @covers ::sanitizeDestination
199    *
200    * @dataProvider providerTestSanitizeDestination
201    *
202    * @see \Drupal\Component\Utility\UrlHelper::isExternal
203    */
204   public function testSanitizeDestinationForGet($input, $output) {
205     $request = new Request();
206     $request->query->set('destination', $input);
207
208     $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
209     $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
210     $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
211
212     $dispatcher = new EventDispatcher();
213     $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'sanitizeDestination'], 100);
214     $dispatcher->dispatch(KernelEvents::REQUEST, $event);
215
216     $this->assertEquals($output, $request->query->get('destination'));
217   }
218
219   /**
220    * Tests that $_REQUEST['destination'] only contain internal URLs.
221    *
222    * @covers ::sanitizeDestination
223    *
224    * @dataProvider providerTestSanitizeDestination
225    *
226    * @see \Drupal\Component\Utility\UrlHelper::isExternal
227    */
228   public function testSanitizeDestinationForPost($input, $output) {
229     $request = new Request();
230     $request->request->set('destination', $input);
231
232     $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
233     $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
234     $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
235
236     $dispatcher = new EventDispatcher();
237     $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'sanitizeDestination'], 100);
238     $dispatcher->dispatch(KernelEvents::REQUEST, $event);
239
240     $this->assertEquals($output, $request->request->get('destination'));
241   }
242
243   /**
244    * Data provider for testSanitizeDestination().
245    */
246   public function providerTestSanitizeDestination() {
247     $data = [];
248     // Standard internal example node path is present in the 'destination'
249     // parameter.
250     $data[] = ['node', 'node'];
251     // Internal path with one leading slash is allowed.
252     $data[] = ['/example.com', '/example.com'];
253     // External URL without scheme is not allowed.
254     $data[] = ['//example.com/test', ''];
255     // Internal URL using a colon is allowed.
256     $data[] = ['example:test', 'example:test'];
257     // External URL is not allowed.
258     $data[] = ['http://example.com', ''];
259     // Javascript URL is allowed because it is treated as an internal URL.
260     $data[] = ['javascript:alert(0)', 'javascript:alert(0)'];
261
262     return $data;
263   }
264
265 }