Skip to content

Commit 58addf0

Browse files
committed
Merge branch 'hotfix/21.0.9' into develop
2 parents e6c33e1 + b9d637f commit 58addf0

File tree

21 files changed

+287
-98
lines changed

21 files changed

+287
-98
lines changed

src/lib/src/ActionRouter/ActionProcessor.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@ public function processAction( string $classOrSlug, array $data = [] ) :ActionRe
2929
}
3030

3131
/**
32+
* SECURITY FIX: Strip action_overrides from user input
33+
* Security controls should never be controllable by user input, even from "authenticated" sources.
34+
* This prevents CSRF bypass attacks where attackers send action_overrides[is_nonce_verify_required]=false
35+
* Integrations that legitimately need overrides (like MainWP) should set them programmatically
36+
* AFTER action creation using setActionOverride() method.
3237
* @throws ActionDoesNotExistException
3338
*/
3439
public function getAction( string $classOrSlug, array $data ) :Actions\BaseAction {
3540
$action = ActionsMap::ActionFromSlug( $classOrSlug );
3641
if ( empty( $action ) ) {
37-
throw new ActionDoesNotExistException( 'There was no action handler available for '.$classOrSlug );
42+
throw new ActionDoesNotExistException( 'There was no action handler available for '.esc_html( $classOrSlug ) );
3843
}
44+
unset( $data[ 'action_overrides' ] );
3945
return new $action( $data );
4046
}
4147
}

src/lib/src/ActionRouter/Actions/BaseAction.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,33 @@ protected function getActionOverrides() :array {
142142
return $this->action_data[ 'action_overrides' ] ?? [];
143143
}
144144

145+
/**
146+
* Set action override programmatically (for trusted integrations like MainWP)
147+
*
148+
* This method allows trusted integrations to set action overrides AFTER action creation,
149+
* ensuring security controls never come from user input paths.
150+
*
151+
* SECURITY NOTE: This method should ONLY be called in trusted contexts where:
152+
* - The caller has verified authentication (e.g., MainWP authenticated requests)
153+
* - The override is set programmatically, not from user input
154+
* - The action object has already been created via ActionProcessor::getAction()
155+
*
156+
* @param string $overrideKey Override key constant (e.g., Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED)
157+
* @param mixed $value Override value (typically boolean for is_nonce_verify_required)
158+
* @return self For method chaining
159+
*/
160+
public function setActionOverride( string $overrideKey, $value ) :self {
161+
// Initialize action_overrides array if it doesn't exist
162+
if ( !isset( $this->action_data[ 'action_overrides' ] ) ) {
163+
$this->action_data[ 'action_overrides' ] = [];
164+
}
165+
166+
// Set the override value
167+
$this->action_data[ 'action_overrides' ][ $overrideKey ] = $value;
168+
169+
return $this;
170+
}
171+
145172
/**
146173
* @throws ActionException
147174
*/

src/lib/src/ActionRouter/Actions/PluginAdmin/PluginAdminPageHandler.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ protected function addSubMenuItems() {
129129
}
130130

131131
public function displayModuleAdminPage() {
132-
echo self::con()->action_router->render( Actions\Render\PageAdminPlugin::class );
132+
echo self::con()->action_router->render(
133+
Actions\Render\PageAdminPlugin::class,
134+
[
135+
Constants::NAV_ID => $this->action_data[ Constants::NAV_ID ] ?? '',
136+
Constants::NAV_SUB_ID => $this->action_data[ Constants::NAV_SUB_ID ] ?? '',
137+
]
138+
);
133139
}
134140
}

src/lib/src/ActionRouter/Actions/Render.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,7 @@ protected function exec() {
2121
$this->setResponse(
2222
self::con()->action_router->action(
2323
$this->action_data[ 'render_action_slug' ],
24-
Services::DataManipulation()->mergeArraysRecursive(
25-
Services::Request()->query,
26-
Services::Request()->post,
27-
\array_filter( $this->action_data[ 'render_action_data' ] ?? [], function ( $item ) {
28-
return !\is_null( $item );
29-
} )
30-
)
24+
\array_filter( $this->action_data[ 'render_action_data' ] ?? [], fn( $item ) => !\is_null( $item ) )
3125
)
3226
);
3327
}

src/lib/src/ActionRouter/Actions/Render/FullPage/Mfa/Components/WpLoginReplicaHeader.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected function getRenderData() :array {
3434
$login_title = get_bloginfo( 'name', 'display' );
3535

3636
/* translators: Login screen title. 1: Login screen name, 2: Network or site name. */
37-
$login_title = sprintf( __( '%1$s ‹ %2$s — WordPress', 'wp-simple-firewall' ), $this->action_data[ 'title' ], $login_title );
37+
$login_title = sprintf( __( '%1$s ‹ %2$s — WordPress', 'wp-simple-firewall' ), esc_html( $this->action_data[ 'title' ] ?? '' ), $login_title );
3838

3939
/**
4040
* Filters the title tag content for login page.
@@ -44,7 +44,7 @@ protected function getRenderData() :array {
4444
* @since 4.9.0
4545
*
4646
*/
47-
$login_title = apply_filters( 'login_title', $login_title, $this->action_data[ 'title' ] );
47+
$login_title = apply_filters( 'login_title', $login_title, esc_html( $this->action_data[ 'title' ] ?? '' ) );
4848

4949
wp_enqueue_style( 'login' );
5050

@@ -136,6 +136,8 @@ protected function getRenderData() :array {
136136
*
137137
*/
138138
$message = apply_filters( 'login_message', $this->action_data[ 'message' ] ?? '' );
139+
// Sanitize output to prevent XSS from URL input or malicious filter callbacks
140+
$message = esc_html( $message );
139141
if ( !empty( $message ) ) {
140142
echo $message."\n";
141143
}
@@ -181,7 +183,7 @@ protected function getRenderData() :array {
181183
}
182184
}
183185

184-
$interimMessage = $this->action_data[ 'interim_message' ] ?? '';
186+
$interimMessage = esc_html( $this->action_data[ 'interim_message' ] ?? '' );
185187

186188
return [
187189
'content' => [

src/lib/src/DBs/ReqLogs/Ops/Handler.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,11 @@ public static function GetTypeName( string $type ) :string {
4848
}
4949
return $type;
5050
}
51+
52+
public static function AllTypes() :array {
53+
return \array_filter(
54+
( new \ReflectionClass( __CLASS__ ) )->getConstants(),
55+
fn( $name ) => \str_starts_with( $name, 'TYPE_' ), \ARRAY_FILTER_USE_KEY
56+
);
57+
}
5158
}

src/lib/src/Modules/Integrations/Lib/MainWP/Client/Actions/Init.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace FernleafSystems\Wordpress\Plugin\Shield\Modules\Integrations\Lib\MainWP\Client\Actions;
44

5+
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\ActionProcessor;
56
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Exceptions\ActionException;
67
use FernleafSystems\Wordpress\Plugin\Shield\Modules\Integrations\Lib\MainWP\{
78
Client\Auth\ReproduceClientAuthByKey,
@@ -40,19 +41,48 @@ public function run() {
4041
return $information;
4142
}, 10, 2 );
4243

43-
// Execute custom actions via MainWP API.
44+
/**
45+
* Execute custom actions via MainWP API.
46+
*
47+
* SECURITY: Action Overrides Handling
48+
*
49+
* MainWP server sends action_overrides (e.g., is_nonce_verify_required=false) in POST data
50+
* to allow server-to-server communication without nonce verification. However, passing these
51+
* overrides directly through user input creates a CSRF bypass vulnerability where attackers
52+
* could send action_overrides[is_nonce_verify_required]=0 to skip CSRF protection.
53+
*
54+
* Solution: ActionProcessor::getAction() strips action_overrides from all input data. We
55+
* extract them here first, then set them programmatically via setActionOverride() only
56+
* after verifying MainWP authentication. This ensures security controls are never
57+
* controllable via user input, while preserving legitimate MainWP server-to-server
58+
* functionality.
59+
*
60+
* We instantiate ActionProcessor directly because we need the action object to
61+
* call setActionOverride() before processing.
62+
*
63+
* @see ActionProcessor::getAction() - strips action_overrides from all input
64+
* @see BaseAction::setActionOverride() - programmatic override setter
65+
* @see SiteCustomAction.php - MainWP server sends overrides
66+
*/
4467
add_filter( 'mainwp_child_extra_execution', function ( $information, $post ) {
4568
$con = self::con();
4669

4770
if ( !empty( $post[ $con->prefix( 'mwp-action' ) ] ) ) {
4871
try {
49-
$response = self::con()
50-
->action_router
51-
->action(
52-
$post[ $con->prefix( 'mwp-action' ) ],
53-
$post[ $con->prefix( 'mwp-params' ) ] ?? []
54-
)
55-
->action_response_data;
72+
$params = $post[ $con->prefix( 'mwp-params' ) ] ?? [];
73+
$actionOverrides = $params[ 'action_overrides' ] ?? [];
74+
75+
$actionSlug = $post[ $con->prefix( 'mwp-action' ) ];
76+
$action = ( new ActionProcessor() )->getAction( $actionSlug, $params );
77+
78+
if ( !empty( $actionOverrides ) && ReproduceClientAuthByKey::Auth() ) {
79+
foreach ( $actionOverrides as $overrideKey => $overrideValue ) {
80+
$action->setActionOverride( $overrideKey, $overrideValue );
81+
}
82+
}
83+
84+
$action->process();
85+
$response = $action->response()->action_response_data;
5686
}
5787
catch ( ActionException $ae ) {
5888
$response = [

src/lib/src/Tables/DataTables/LoadData/ActivityLog/BuildActivityLogTableData.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace FernleafSystems\Wordpress\Plugin\Shield\Tables\DataTables\LoadData\ActivityLog;
44

5+
use FernleafSystems\Wordpress\Plugin\Shield\Tables\DataTables\Build\SearchPanes\BuildDataForDays;
56
use FernleafSystems\Wordpress\Plugin\Shield\DBs\ActivityLogs\{
67
LoadLogs,
78
LogRecord
@@ -19,12 +20,16 @@ class BuildActivityLogTableData extends BaseBuildTableData {
1920
*/
2021
private $log;
2122

23+
protected function getSearchPanesDataBuilder() :BuildSearchPanesData {
24+
return new BuildSearchPanesData();
25+
}
26+
2227
protected function loadRecordsWithDirectQuery() :array {
2328
return $this->loadRecordsWithSearch();
2429
}
2530

2631
protected function getSearchPanesData() :array {
27-
return ( new BuildSearchPanesData() )->build();
32+
return $this->getSearchPanesDataBuilder()->build();
2833
}
2934

3035
/**
@@ -55,6 +60,20 @@ function ( $log ) {
5560
) );
5661
}
5762

63+
protected function validateSearchPanes( array $searchPanes ) :array {
64+
foreach ( $searchPanes as $column => &$values ) {
65+
switch ( $column ) {
66+
case 'event':
67+
$values = \array_filter( $values, fn( $event ) => !empty( $event ) && self::con()->comps->events->eventExists( $event ) );
68+
break;
69+
default:
70+
$values = $this->validateCommonColumn( $column, $values );
71+
break;
72+
}
73+
}
74+
return \array_filter( $searchPanes );
75+
}
76+
5877
/**
5978
* The `Where`s need to align with the structure of the Query called from getRecords()
6079
*/
@@ -77,11 +96,11 @@ protected function buildWheresFromSearchParams() :array {
7796
case 'ip':
7897
$wheres[] = sprintf( "`ips`.`ip`=INET6_ATON('%s')", \array_pop( $selected ) );
7998
break;
80-
case 'user':
81-
if ( \count( $selected ) > 0 ) {
82-
$wheres[] = sprintf( "`req`.`uid` IN (%s)", \implode( ',', \array_values( $selected ) ) );
83-
}
84-
break;
99+
case 'user':
100+
if ( \count( $selected ) > 0 ) {
101+
$wheres[] = sprintf( "`req`.`uid` IN (%s)", \implode( ',', $selected ) );
102+
}
103+
break;
85104
default:
86105
break;
87106
}

src/lib/src/Tables/DataTables/LoadData/ActivityLog/BuildSearchPanesData.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
use FernleafSystems\Wordpress\Plugin\Shield\Modules\PluginControllerConsumer;
66
use FernleafSystems\Wordpress\Plugin\Shield\Tables\DataTables\Build\SearchPanes\BuildDataForDays;
77
use FernleafSystems\Wordpress\Plugin\Shield\Tables\DataTables\Build\SearchPanes\BuildDataForUsers;
8+
use FernleafSystems\Wordpress\Plugin\Shield\Tables\DataTables\LoadData\BaseBuildSearchPanesData;
89
use FernleafSystems\Wordpress\Services\Services;
910

10-
class BuildSearchPanesData {
11+
class BuildSearchPanesData extends BaseBuildSearchPanesData {
1112

1213
use PluginControllerConsumer;
1314

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare( strict_types=1 );
2+
3+
namespace FernleafSystems\Wordpress\Plugin\Shield\Tables\DataTables\LoadData;
4+
5+
use FernleafSystems\Wordpress\Plugin\Shield\Modules\PluginControllerConsumer;
6+
7+
class BaseBuildSearchPanesData {
8+
9+
use PluginControllerConsumer;
10+
11+
public function build() :array {
12+
return [];
13+
}
14+
}

0 commit comments

Comments
 (0)