mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #65 from silverstripe-security/pulls/4.2/ss-2018-009
[SS-2018-009] Allow forced redirects to HTTPS for responses with basic authentication
This commit is contained in:
commit
c3e5ab2258
@ -691,6 +691,11 @@ if (!Director::isDev()) {
|
|||||||
Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as
|
Forcing HTTPS so requires a certificate to be purchased or obtained through a vendor such as
|
||||||
[lets encrypt](https://letsencrypt.org/) and configured on your web server.
|
[lets encrypt](https://letsencrypt.org/) and configured on your web server.
|
||||||
|
|
||||||
|
Note that by default enabling SSL will also enable `CanonicalURLMiddleware::forceBasicAuthToSSL` which will detect
|
||||||
|
and automatically redirect any requests with basic authentication headers to first be served over HTTPS. You can
|
||||||
|
disable this behaviour using `CanonicalURLMiddleware::singleton()->setForceBasicAuthToSSL(false)`, or via Injector
|
||||||
|
configuration in YAML.
|
||||||
|
|
||||||
We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell SilverStripe to
|
We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell SilverStripe to
|
||||||
use a [secure session](https://docs.silverstripe.org/en/3/developer_guides/cookies_and_sessions/sessions/#secure-session-cookie).
|
use a [secure session](https://docs.silverstripe.org/en/3/developer_guides/cookies_and_sessions/sessions/#secure-session-cookie).
|
||||||
To do this, you may set the `cookie_secure` parameter to `true` in your `config.yml` for `Session`
|
To do this, you may set the `cookie_secure` parameter to `true` in your `config.yml` for `Session`
|
||||||
|
@ -34,6 +34,14 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
|||||||
*/
|
*/
|
||||||
protected $forceSSL = false;
|
protected $forceSSL = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if we should automatically redirect basic auth requests to HTTPS. A null value (default) will
|
||||||
|
* cause this property to return the value of the forceSSL property.
|
||||||
|
*
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
protected $forceBasicAuthToSSL = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect type
|
* Redirect type
|
||||||
*
|
*
|
||||||
@ -137,6 +145,29 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool|null $forceBasicAuth
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setForceBasicAuthToSSL($forceBasicAuth)
|
||||||
|
{
|
||||||
|
$this->forceBasicAuthToSSL = $forceBasicAuth;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getForceBasicAuthToSSL()
|
||||||
|
{
|
||||||
|
// Check if explicitly set
|
||||||
|
if (isset($this->forceBasicAuthToSSL)) {
|
||||||
|
return $this->forceBasicAuthToSSL;
|
||||||
|
}
|
||||||
|
// If not explicitly set, default to on if ForceSSL is on
|
||||||
|
return $this->getForceSSL();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate response for the given request
|
* Generate response for the given request
|
||||||
*
|
*
|
||||||
@ -152,7 +183,16 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
|||||||
return $redirect;
|
return $redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $delegate($request);
|
/** @var HTTPResponse $response */
|
||||||
|
$response = $delegate($request);
|
||||||
|
if ($this->hasBasicAuthPrompt($response)
|
||||||
|
&& $request->getScheme() !== 'https'
|
||||||
|
&& $this->getForceBasicAuthToSSL()
|
||||||
|
) {
|
||||||
|
return $this->redirectToScheme($request, 'https');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -190,14 +230,7 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild url for request
|
return $this->redirectToScheme($request, $scheme, $host);
|
||||||
$url = Controller::join_links("{$scheme}://{$host}", Director::baseURL(), $request->getURL(true));
|
|
||||||
|
|
||||||
// Force redirect
|
|
||||||
$response = new HTTPResponse();
|
|
||||||
$response->redirect($url, $this->getRedirectType());
|
|
||||||
HTTP::add_cache_headers($response);
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -224,7 +257,7 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
|||||||
* Return a valid request, if one is available, or null if none is available
|
* Return a valid request, if one is available, or null if none is available
|
||||||
*
|
*
|
||||||
* @param HTTPRequest $request
|
* @param HTTPRequest $request
|
||||||
* @return mixed|null
|
* @return HTTPRequest|null
|
||||||
*/
|
*/
|
||||||
protected function getOrValidateRequest(HTTPRequest $request = null)
|
protected function getOrValidateRequest(HTTPRequest $request = null)
|
||||||
{
|
{
|
||||||
@ -328,4 +361,42 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
|||||||
}
|
}
|
||||||
return empty($enabledEnvs) || in_array(Director::get_environment_type(), $enabledEnvs);
|
return empty($enabledEnvs) || in_array(Director::get_environment_type(), $enabledEnvs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the executed middlewares have added a basic authentication prompt
|
||||||
|
*
|
||||||
|
* @param HTTPResponse $response
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function hasBasicAuthPrompt(HTTPResponse $response = null)
|
||||||
|
{
|
||||||
|
if (!$response) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ($response->getStatusCode() === 401 && $response->getHeader('WWW-Authenticate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect the current URL to the specified HTTP scheme
|
||||||
|
*
|
||||||
|
* @param HTTPRequest $request
|
||||||
|
* @param string $scheme
|
||||||
|
* @param string $host
|
||||||
|
* @return HTTPResponse
|
||||||
|
*/
|
||||||
|
protected function redirectToScheme(HTTPRequest $request, $scheme, $host = null)
|
||||||
|
{
|
||||||
|
if (!$host) {
|
||||||
|
$host = $request->getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = Controller::join_links("{$scheme}://{$host}", Director::baseURL(), $request->getURL(true));
|
||||||
|
|
||||||
|
// Force redirect
|
||||||
|
$response = HTTPResponse::create();
|
||||||
|
$response->redirect($url, $this->getRedirectType());
|
||||||
|
HTTP::add_cache_headers($response);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
98
tests/php/Control/Middleware/CanonicalURLMiddlewareTest.php
Normal file
98
tests/php/Control/Middleware/CanonicalURLMiddlewareTest.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Control\Tests\Middleware;
|
||||||
|
|
||||||
|
use SilverStripe\Control\HTTPRequest;
|
||||||
|
use SilverStripe\Control\HTTPResponse;
|
||||||
|
use SilverStripe\Control\Middleware\CanonicalURLMiddleware;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
|
||||||
|
class CanonicalURLMiddlewareTest extends SapphireTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Stub middleware class, partially mocked
|
||||||
|
*
|
||||||
|
* @var CanonicalURLMiddleware
|
||||||
|
*/
|
||||||
|
protected $middleware;
|
||||||
|
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
/** @var CanonicalURLMiddleware $middleware */
|
||||||
|
$this->middleware = $this->getMockBuilder(CanonicalURLMiddleware::class)
|
||||||
|
->setMethods(['getRedirect'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$this->middleware->expects($this->any())->method('getRedirect')->willReturn(false);
|
||||||
|
|
||||||
|
$this->middleware->setForceBasicAuthToSSL(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpsIsForcedForBasicAuth()
|
||||||
|
{
|
||||||
|
$this->middleware->expects($this->once())->method('getRedirect');
|
||||||
|
|
||||||
|
$request = new HTTPRequest('GET', '/');
|
||||||
|
$mockResponse = (new HTTPResponse)
|
||||||
|
->addHeader('WWW-Authenticate', 'basic')
|
||||||
|
->setStatusCode(401);
|
||||||
|
|
||||||
|
$result = $this->middleware->process($request, function () use ($mockResponse) {
|
||||||
|
return $mockResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertNotSame($mockResponse, $result, 'New response is created and returned');
|
||||||
|
$this->assertEquals(301, $result->getStatusCode(), 'Basic auth responses are redirected');
|
||||||
|
$this->assertContains('https://', $result->getHeader('Location'), 'HTTPS is in the redirect location');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMiddlewareDelegateIsReturnedWhenBasicAuthRedirectIsDisabled()
|
||||||
|
{
|
||||||
|
$this->middleware->expects($this->once())->method('getRedirect');
|
||||||
|
$this->middleware->setForceBasicAuthToSSL(false);
|
||||||
|
|
||||||
|
$request = new HTTPRequest('GET', '/');
|
||||||
|
$mockResponse = (new HTTPResponse)
|
||||||
|
->addHeader('WWW-Authenticate', 'basic')
|
||||||
|
->setStatusCode(401);
|
||||||
|
|
||||||
|
$result = $this->middleware->process($request, function () use ($mockResponse) {
|
||||||
|
return $mockResponse;
|
||||||
|
});
|
||||||
|
$this->assertSame($mockResponse, $result, 'Response returned verbatim with auto redirect disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMiddlewareDelegateIsReturnedWhenNoBasicAuthIsPresent()
|
||||||
|
{
|
||||||
|
$this->middleware->expects($this->once())->method('getRedirect');
|
||||||
|
|
||||||
|
$request = new HTTPRequest('GET', '/');
|
||||||
|
$mockResponse = (new HTTPResponse)->addHeader('Foo', 'bar');
|
||||||
|
|
||||||
|
$result = $this->middleware->process($request, function () use ($mockResponse) {
|
||||||
|
return $mockResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->assertSame($mockResponse, $result, 'Non basic-auth responses are returned verbatim');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetForceBasicAuthToSSL()
|
||||||
|
{
|
||||||
|
$this->middleware->setForceBasicAuthToSSL(null);
|
||||||
|
|
||||||
|
$this->middleware->setForceSSL(true);
|
||||||
|
$this->assertTrue($this->middleware->getForceBasicAuthToSSL(), 'Default falls over to forceSSL');
|
||||||
|
|
||||||
|
$this->middleware->setForceSSL(false);
|
||||||
|
$this->assertFalse($this->middleware->getForceBasicAuthToSSL(), 'Default falls over to forceSSL');
|
||||||
|
|
||||||
|
$this->middleware->setForceBasicAuthToSSL(true);
|
||||||
|
$this->assertTrue($this->middleware->getForceBasicAuthToSSL(), 'Explicitly set is returned');
|
||||||
|
|
||||||
|
$this->middleware->setForceBasicAuthToSSL(false);
|
||||||
|
$this->middleware->setForceSSL(true);
|
||||||
|
$this->assertFalse($this->middleware->getForceBasicAuthToSSL(), 'Explicitly set is returned');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user