Skip to content

Commit 6e3156d

Browse files
authored
Merge pull request #1473 from wintercms/fix/security-improvements
2 parents 164c625 + 2b1bb7c commit 6e3156d

File tree

14 files changed

+994
-61
lines changed

14 files changed

+994
-61
lines changed

modules/backend/ServiceProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ protected function registerBackendSettings()
303303
'description' => 'backend::lang.myaccount.menu_description',
304304
'category' => SettingsManager::CATEGORY_MYSETTINGS,
305305
'icon' => 'icon-user',
306-
'url' => Backend::url('backend/users/myaccount'),
306+
'url' => Backend::url('backend/myaccount'),
307307
'order' => 500,
308308
'context' => 'mysettings',
309309
'keywords' => 'backend::lang.myaccount.menu_keywords'

modules/backend/classes/Controller.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,16 @@ public function run($action = null, $params = [])
319319
*/
320320
elseif (
321321
($handler = post('_handler')) &&
322-
$this->verifyCsrfToken() &&
323-
($handlerResponse = $this->runAjaxHandler($handler)) &&
324-
$handlerResponse !== true
322+
$this->verifyCsrfToken()
325323
) {
326-
$result = $handlerResponse;
324+
$this->validateHandlerName($handler);
325+
326+
if (
327+
($handlerResponse = $this->runAjaxHandler($handler)) &&
328+
$handlerResponse !== true
329+
) {
330+
$result = $handlerResponse;
331+
}
327332
}
328333

329334
/*
@@ -476,6 +481,18 @@ public function getAjaxHandler()
476481
return null;
477482
}
478483

484+
/**
485+
* Validates the AJAX handler name follows the expected format.
486+
*
487+
* @throws \Winter\Storm\Exception\SystemException if the handler name is invalid
488+
*/
489+
protected function validateHandlerName(string $handler): void
490+
{
491+
if (!preg_match('/^(?:\w+\:{2})?on[A-Z]{1}[\w+]*$/', $handler)) {
492+
throw new SystemException(Lang::get('backend::lang.ajax_handler.invalid_name', ['name' => $handler]));
493+
}
494+
}
495+
479496
/**
480497
* This method is used internally.
481498
* Invokes a controller event handler and loads the supplied partials.
@@ -487,9 +504,7 @@ protected function execAjaxHandlers()
487504
/*
488505
* Validate the handler name
489506
*/
490-
if (!preg_match('/^(?:\w+\:{2})?on[A-Z]{1}[\w+]*$/', $handler)) {
491-
throw new SystemException(Lang::get('backend::lang.ajax_handler.invalid_name', ['name'=>$handler]));
492-
}
507+
$this->validateHandlerName($handler);
493508

494509
/*
495510
* Validate the handler partial list

modules/backend/controllers/Index.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected function checkPermissionRedirect()
7575
if ($first = array_first(BackendMenu::listMainMenuItems())) {
7676
return Redirect::intended($first->url);
7777
}
78-
return Backend::redirect('backend/users/myaccount');
78+
return Backend::redirect('backend/myaccount');
7979
}
8080
}
8181
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Backend\Controllers;
4+
5+
use Backend\Behaviors\FormController;
6+
use Backend\Classes\Controller;
7+
use Backend\Facades\BackendAuth;
8+
use Backend\Facades\BackendMenu;
9+
use System\Classes\SettingsManager;
10+
11+
/**
12+
* My Account controller
13+
*
14+
* Allows any authenticated backend user to manage their own account settings.
15+
* Isolated from the Users controller to prevent privilege escalation via
16+
* handler dispatch on a controller with degraded permissions.
17+
*
18+
* @package winter\wn-backend-module
19+
* @author Winter CMS
20+
*/
21+
class MyAccount extends Controller
22+
{
23+
/**
24+
* @var array Extensions implemented by this controller.
25+
*/
26+
public $implement = [
27+
FormController::class,
28+
];
29+
30+
/**
31+
* @var array Permissions required to view this page.
32+
* Empty array — any logged-in user can access their own account.
33+
*/
34+
public $requiredPermissions = [];
35+
36+
/**
37+
* @var string HTML body tag class
38+
*/
39+
public $bodyClass = 'compact-container';
40+
41+
public $formLayout = 'sidebar';
42+
43+
/**
44+
* Constructor.
45+
*/
46+
public function __construct()
47+
{
48+
parent::__construct();
49+
50+
BackendMenu::setContext('Winter.System', 'system', 'users');
51+
SettingsManager::setContext('Winter.Backend', 'myaccount');
52+
}
53+
54+
/**
55+
* My Account page
56+
*/
57+
public function index()
58+
{
59+
$this->pageTitle = 'backend::lang.myaccount.menu_label';
60+
return $this->asExtension('FormController')->update($this->user->id, 'myaccount');
61+
}
62+
63+
/**
64+
* Save handler for the My Account form
65+
*/
66+
public function index_onSave()
67+
{
68+
$result = $this->asExtension('FormController')->update_onSave($this->user->id, 'myaccount');
69+
70+
/*
71+
* If the password or login name has been updated, reauthenticate the user
72+
*/
73+
$loginChanged = $this->user->login != post('User[login]');
74+
$passwordChanged = strlen(post('User[password]'));
75+
if ($loginChanged || $passwordChanged) {
76+
BackendAuth::login($this->user->reload(), true);
77+
}
78+
79+
return $result;
80+
}
81+
}

modules/backend/controllers/Users.php

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ public function __construct()
5555
{
5656
parent::__construct();
5757

58-
if ($this->action == 'myaccount') {
59-
$this->requiredPermissions = null;
60-
}
61-
6258
BackendMenu::setContext('Winter.System', 'system', 'users');
6359
SettingsManager::setContext('Winter.System', 'administrators');
6460
}
@@ -123,9 +119,9 @@ public function formBeforeCreate($model)
123119
*/
124120
public function update($recordId, $context = null)
125121
{
126-
// Users cannot edit themselves, only use My Settings
122+
// Users cannot edit themselves, only use My Account
127123
if ($context != 'myaccount' && $recordId == $this->user->id) {
128-
return Backend::redirect('backend/users/myaccount');
124+
return Backend::redirect('backend/myaccount');
129125
}
130126

131127
return $this->asExtension('FormController')->update($recordId, $context);
@@ -158,7 +154,7 @@ public function update_onImpersonateUser($recordId)
158154

159155
Flash::success(Lang::get('backend::lang.account.impersonate_success'));
160156

161-
return Backend::redirect('backend/users/myaccount');
157+
return Backend::redirect('backend/myaccount');
162158
}
163159

164160
/**
@@ -176,33 +172,11 @@ public function update_onUnsuspendUser($recordId)
176172
}
177173

178174
/**
179-
* My Settings controller
175+
* Backward compatibility redirect to the new MyAccount controller.
180176
*/
181177
public function myaccount()
182178
{
183-
SettingsManager::setContext('Winter.Backend', 'myaccount');
184-
185-
$this->pageTitle = 'backend::lang.myaccount.menu_label';
186-
return $this->update($this->user->id, 'myaccount');
187-
}
188-
189-
/**
190-
* Proxy update onSave event
191-
*/
192-
public function myaccount_onSave()
193-
{
194-
$result = $this->asExtension('FormController')->update_onSave($this->user->id, 'myaccount');
195-
196-
/*
197-
* If the password or login name has been updated, reauthenticate the user
198-
*/
199-
$loginChanged = $this->user->login != post('User[login]');
200-
$passwordChanged = strlen(post('User[password]'));
201-
if ($loginChanged || $passwordChanged) {
202-
BackendAuth::login($this->user->reload(), true);
203-
}
204-
205-
return $result;
179+
return Backend::redirect('backend/myaccount');
206180
}
207181

208182
/**
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ===================================
2+
# Form Behavior Config
3+
# ===================================
4+
5+
name: backend::lang.user.name
6+
form: ~/modules/backend/models/user/fields.yaml
7+
modelClass: Backend\Models\User
8+
defaultRedirect: backend/myaccount
9+
10+
update:
11+
redirect: backend/myaccount
12+
redirectClose: backend/myaccount
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php if ($this->user->hasAccess('backend.manage_users')): ?>
2+
<?php Block::put('breadcrumb') ?>
3+
<ul>
4+
<li><a href="<?= Backend::url('backend/users') ?>"><?= e(trans('backend::lang.user.menu_label')) ?></a></li>
5+
<li><?= e(trans($this->pageTitle)) ?></li>
6+
</ul>
7+
<?php Block::endPut() ?>
8+
<?php endif ?>
9+
10+
<?php if (!$this->fatalError): ?>
11+
12+
<?php Block::put('form-contents') ?>
13+
<div class="layout">
14+
15+
<div class="layout-row">
16+
<?= $this->formRenderOutsideFields() ?>
17+
<?= $this->formRenderPrimaryTabs() ?>
18+
</div>
19+
20+
<div class="form-buttons">
21+
<div class="loading-indicator-container">
22+
<button
23+
type="submit"
24+
data-request="onSave"
25+
data-browser-validate
26+
data-request-data="redirect:0"
27+
data-hotkey="ctrl+s, cmd+s"
28+
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
29+
class="btn btn-primary">
30+
<?= e(trans('backend::lang.form.save')) ?>
31+
</button>
32+
</div>
33+
</div>
34+
35+
</div>
36+
<?php Block::endPut() ?>
37+
38+
<?php Block::put('form-sidebar') ?>
39+
<div class="hide-tabs"><?= $this->formRenderSecondaryTabs() ?></div>
40+
<?php Block::endPut() ?>
41+
42+
<?php Block::put('body') ?>
43+
<?= Form::open(['class'=>'layout stretch']) ?>
44+
<?= $this->makeLayout('form-with-sidebar') ?>
45+
<?= Form::close() ?>
46+
<?php Block::endPut() ?>
47+
48+
<?php else: ?>
49+
<div class="control-breadcrumb">
50+
<?= Block::placeholder('breadcrumb') ?>
51+
</div>
52+
<div class="padded-container">
53+
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
54+
<p><a href="<?= Backend::url('backend') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
55+
</div>
56+
<?php endif ?>

modules/backend/lang/en/lang.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,7 @@
168168
'deleted_at' => 'Deleted at',
169169
'show_deleted' => 'Show deleted',
170170
'self_escalation_denied' => 'You cannot modify your own role, permissions, or superuser status.',
171-
'superuser_grant_denied' => 'Only superusers can grant superuser status or modify other superuser accounts.',
172-
'manage_users_denied' => 'You do not have permission to manage other administrators.',
171+
'cannot_manage_user' => 'You do not have permission to manage this administrator.',
173172
'throttle_tab' => 'Failed Logins',
174173
'throttle_tab_label' => 'Failed Login Records',
175174
'throttle_comment' => 'View failed login attempts for this user. These records are automatically generated when login attempts fail. Users are suspended after exceeding the attempt limit.',

0 commit comments

Comments
 (0)