Merge pull request #4830 from open-sausages/pulls/3/fix-querystring-stage

API Disable unauthenticated get parameter access to site stage mode
This commit is contained in:
Ingo Schommer 2015-12-10 10:44:43 +13:00
commit 0175167761
4 changed files with 127 additions and 72 deletions

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Initialises the versioned stage when a request is made. * Initialises the versioned stage when a request is made.
* *
@ -8,7 +9,37 @@
class VersionedRequestFilter implements RequestFilter { class VersionedRequestFilter implements RequestFilter {
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) { public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) {
Versioned::choose_site_stage($session); // Bootstrap session so that Session::get() accesses the right instance
$dummyController = new Controller();
$dummyController->setSession($session);
$dummyController->setRequest($request);
$dummyController->pushCurrent();
// Block non-authenticated users from setting the stage mode
if(!Versioned::can_choose_site_stage($request)) {
$permissionMessage = sprintf(
_t(
"ContentController.DRAFT_SITE_ACCESS_RESTRICTION",
'You must log in with your CMS password in order to view the draft or archived content. '.
'<a href="%s">Click here to go back to the published site.</a>'
),
Controller::join_links(Director::baseURL(), $request->getURL(), "?stage=Live")
);
// Force output since RequestFilter::preRequest doesn't support response overriding
$response = Security::permissionFailure($dummyController, $permissionMessage);
$session->inst_save();
$dummyController->popCurrent();
// Prevent output in testing
if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
return false;
}
$response->output();
die;
}
Versioned::choose_site_stage();
$dummyController->popCurrent();
return true; return true;
} }

View File

@ -1084,6 +1084,26 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
//-----------------------------------------------------------------------------------------------// //-----------------------------------------------------------------------------------------------//
/**
* Determine if the current user is able to set the given site stage / archive
*
* @param SS_HTTPRequest $request
* @return bool
*/
public static function can_choose_site_stage($request) {
// Request is allowed if stage isn't being modified
if((!$request->getVar('stage') || $request->getVar('stage') === static::get_live_stage())
&& !$request->getVar('archiveDate')
) {
return true;
}
// Check permissions with member ID in session.
$member = Member::currentUser();
$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
return $member && Permission::checkMember($member, $permissions);
}
/** /**
* Choose the stage the site is currently on. * Choose the stage the site is currently on.
* *
@ -1095,14 +1115,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* *
* If neither of these are set, it checks the session, otherwise the stage * If neither of these are set, it checks the session, otherwise the stage
* is set to 'Live'. * is set to 'Live'.
*
* @param Session $session Optional session within which to store the resulting stage
*/ */
public static function choose_site_stage($session = null) { public static function choose_site_stage() {
// Check any pre-existing session mode // Check any pre-existing session mode
$preexistingMode = $session $preexistingMode = Session::get('readingMode');
? $session->inst_get('readingMode')
: Session::get('readingMode');
// Determine the reading mode // Determine the reading mode
if(isset($_GET['stage'])) { if(isset($_GET['stage'])) {
@ -1124,12 +1140,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
if(($preexistingMode && $preexistingMode !== $mode) if(($preexistingMode && $preexistingMode !== $mode)
|| (!$preexistingMode && $mode !== self::DEFAULT_MODE) || (!$preexistingMode && $mode !== self::DEFAULT_MODE)
) { ) {
if($session) {
$session->inst_set('readingMode', $mode);
} else {
Session::set('readingMode', $mode); Session::set('readingMode', $mode);
} }
}
if(!headers_sent() && !Director::is_cli()) { if(!headers_sent() && !Director::is_cli()) {
if(Versioned::current_stage() == 'Live') { if(Versioned::current_stage() == 'Live') {

View File

@ -208,6 +208,7 @@ class Security extends Controller implements TemplateGlobalProvider {
* *
* The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
* to log in. * to log in.
* @return SS_HTTPResponse
*/ */
public static function permissionFailure($controller = null, $messageSet = null) { public static function permissionFailure($controller = null, $messageSet = null) {
self::set_ignore_disallowed_actions(true); self::set_ignore_disallowed_actions(true);
@ -226,7 +227,8 @@ class Security extends Controller implements TemplateGlobalProvider {
} }
} }
return $response; return $response;
} else { }
// Prepare the messageSet provided // Prepare the messageSet provided
if(!$messageSet) { if(!$messageSet) {
if($configMessageSet = static::config()->get('default_message_set')) { if($configMessageSet = static::config()->get('default_message_set')) {
@ -293,13 +295,11 @@ class Security extends Controller implements TemplateGlobalProvider {
// Audit logging hook // Audit logging hook
$controller->extend('permissionDenied', $member); $controller->extend('permissionDenied', $member);
$controller->redirect( return $controller->redirect(
Config::inst()->get('Security', 'login_url') Config::inst()->get('Security', 'login_url')
. "?BackURL=" . urlencode($_SERVER['REQUEST_URI']) . "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
); );
} }
return;
}
public function init() { public function init() {
parent::init(); parent::init();

View File

@ -599,6 +599,8 @@ class VersionedTest extends SapphireTest {
*/ */
public function testReadingPersistent() { public function testReadingPersistent() {
$session = Injector::inst()->create('Session', array()); $session = Injector::inst()->create('Session', array());
$adminID = $this->logInWithPermission('ADMIN');
$session->inst_set('loggedInAs', $adminID);
// Set to stage // Set to stage
Director::test('/?stage=Stage', null, $session); Director::test('/?stage=Stage', null, $session);
@ -630,10 +632,11 @@ class VersionedTest extends SapphireTest {
// Test that session doesn't redundantly store the default stage if it doesn't need to // Test that session doesn't redundantly store the default stage if it doesn't need to
$session2 = Injector::inst()->create('Session', array()); $session2 = Injector::inst()->create('Session', array());
$session2->inst_set('loggedInAs', $adminID);
Director::test('/', null, $session2); Director::test('/', null, $session2);
$this->assertEmpty($session2->inst_changedData()); $this->assertArrayNotHasKey('readingMode', $session2->inst_changedData());
Director::test('/?stage=Live', null, $session2); Director::test('/?stage=Live', null, $session2);
$this->assertEmpty($session2->inst_changedData()); $this->assertArrayNotHasKey('readingMode', $session2->inst_changedData());
// Test choose_site_stage // Test choose_site_stage
Session::set('readingMode', 'Stage.Stage'); Session::set('readingMode', 'Stage.Stage');
@ -647,6 +650,15 @@ class VersionedTest extends SapphireTest {
$this->assertEquals('Stage.Live', Versioned::get_reading_mode()); $this->assertEquals('Stage.Live', Versioned::get_reading_mode());
} }
/**
* Test that stage parameter is blocked by non-administrative users
*/
public function testReadingModeSecurity() {
$this->setExpectedException('SS_HTTPResponse_Exception', 'Invalid request');
$session = Injector::inst()->create('Session', array());
$result = Director::test('/?stage=Stage', null, $session);
}
/** /**
* Ensures that the latest version of a record is the expected value * Ensures that the latest version of a record is the expected value
* *