| @@ -146,16 +146,49 @@ | ||
| 146 | 146 | $this->update_last_activity(); |
| 147 | 147 | } |
| 148 | 148 | |
| 149 | 149 | /** |
| 150 | + * Get transient key for server-side session last activity. | |
| 151 | + * | |
| 152 | + * @param int|null $user_id User ID. | |
| 153 | + * @return string|null | |
| 154 | + */ | |
| 155 | + private function get_session_activity_key( $user_id = null ): ?string { | |
| 156 | + $user_id = $user_id ?? get_current_user_id(); | |
| 157 | + if ( ! $user_id ) { | |
| 158 | + return null; | |
| 159 | + } | |
| 160 | + | |
| 161 | + $token = wp_get_session_token(); | |
| 162 | + if ( ! $token ) { | |
| 163 | + return 'wpdef_last_act_' . $user_id; | |
| 164 | + } | |
| 165 | + | |
| 166 | + return 'wpdef_last_act_' . $user_id . '_' . substr( md5( $token ), 0, 16 ); | |
| 167 | + } | |
| 168 | + | |
| 169 | + /** | |
| 150 | 170 | * Retrieves the last activity timestamp. |
| 151 | 171 | * |
| 152 | 172 | * @return int|null Last activity timestamp or null if not set. |
| 153 | 173 | */ |
| 154 | 174 | private function get_last_activity() { |
| 155 | - // Check for client-side cookie first to avoid unnecessary database queries. | |
| 175 | + // First check server-side session state if available to prevent client tampering. | |
| 176 | + $key = $this->get_session_activity_key(); | |
| 177 | + if ( $key ) { | |
| 178 | + $server_activity = get_transient( $key ); | |
| 179 | + if ( is_numeric( $server_activity ) && (int) $server_activity > 0 ) { | |
| 180 | + return (int) $server_activity; | |
| 181 | + } | |
| 182 | + } | |
| 183 | + | |
| 184 | + // Fallback to client-side cookie if server transient is missing. | |
| 156 | 185 | if ( isset( $_COOKIE['wpdef_last_activity'] ) && is_numeric( $_COOKIE['wpdef_last_activity'] ) ) { |
| 157 | - return (int) $_COOKIE['wpdef_last_activity']; | |
| 186 | + $timestamp = (int) $_COOKIE['wpdef_last_activity']; | |
| 187 | + $now = time(); | |
| 188 | + | |
| 189 | + // Clamp future timestamps to current server time to prevent session timeout bypass. | |
| 190 | + return min( $timestamp, $now ); | |
| 158 | 191 | } |
| 159 | 192 | |
| 160 | 193 | return null; |
| 161 | 194 | } |
| @@ -178,8 +211,14 @@ | ||
| 178 | 211 | ); |
| 179 | 212 | // Fires during session timeout. |
| 180 | 213 | do_action( 'wpdef_session_timeout', $user_id ); |
| 181 | 214 | } |
| 215 | + | |
| 216 | + $key = $this->get_session_activity_key( $user_id ); | |
| 217 | + if ( $key ) { | |
| 218 | + delete_transient( $key ); | |
| 219 | + } | |
| 220 | + | |
| 182 | 221 | // Get current session token. |
| 183 | 222 | $current_session_token = wp_get_session_token(); |
| 184 | 223 | if ( $current_session_token ) { |
| 185 | 224 | // Get session manager for the user. |
| @@ -300,9 +339,15 @@ | ||
| 300 | 339 | * |
| 301 | 340 | * @return void |
| 302 | 341 | */ |
| 303 | 342 | public function update_last_activity() { |
| 304 | - $current_time = time(); | |
| 343 | + $current_time = time(); | |
| 344 | + | |
| 345 | + $key = $this->get_session_activity_key(); | |
| 346 | + if ( $key ) { | |
| 347 | + set_transient( $key, $current_time, DAY_IN_SECONDS ); | |
| 348 | + } | |
| 349 | + | |
| 305 | 350 | $last_activity = $this->get_last_activity(); |
| 306 | 351 | |
| 307 | 352 | // Only update the cookie if minutes have passed since the last update. |
| 308 | 353 | if ( ! is_null( $last_activity ) || ( $current_time - $last_activity > MINUTE_IN_SECONDS ) ) { |