Skip to content

Commit 6bc0db3

Browse files
committed
More fixes
1 parent 9d3ee7d commit 6bc0db3

3 files changed

Lines changed: 374 additions & 5 deletions

File tree

classes/class-suggested-tasks.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ public function was_task_completed( $task_id ): bool {
188188
* Primarly this is used for deeplinking, ie user is testing if the emails are working
189189
* He gets an email with a link which automatically completes the task.
190190
*
191+
* SECURITY FIX: Added token verification to prevent CSRF attacks.
192+
* Tokens are one-time use and expire after 24 hours.
193+
*
191194
* @return void
192195
*/
193196
public function maybe_complete_task() {
@@ -200,6 +203,19 @@ public function maybe_complete_task() {
200203
return;
201204
}
202205

206+
// SECURITY FIX: Verify token to prevent CSRF attacks.
207+
if ( ! isset( $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
208+
return;
209+
}
210+
211+
$provided_token = \sanitize_text_field( \wp_unslash( $_GET['token'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
212+
$user_id = \get_current_user_id();
213+
214+
// Verify the token.
215+
if ( ! $this->verify_task_completion_token( $task_id, $user_id, $provided_token ) ) {
216+
return;
217+
}
218+
203219
if ( ! $this->was_task_completed( $task_id ) ) {
204220
$task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id );
205221

@@ -208,10 +224,71 @@ public function maybe_complete_task() {
208224

209225
// Insert an activity.
210226
$this->insert_activity( $task_id );
227+
228+
// Delete the token after successful use (one-time use).
229+
$this->delete_task_completion_token( $task_id, $user_id );
211230
}
212231
}
213232
}
214233

234+
/**
235+
* Generate a secure token for task completion via email link.
236+
*
237+
* This token prevents CSRF attacks by ensuring only legitimate email
238+
* links can mark tasks as complete.
239+
*
240+
* @param string $task_id The task ID.
241+
* @param int $user_id The user ID.
242+
*
243+
* @return string The generated token.
244+
*/
245+
public function generate_task_completion_token( $task_id, $user_id ) {
246+
// Generate a cryptographically secure random token.
247+
$random = \wp_generate_password( 32, false );
248+
$token = \wp_hash( $task_id . $user_id . $random . \wp_salt( 'auth' ) );
249+
250+
// Store the token in a transient (expires after 24 hours).
251+
\set_transient(
252+
'prpl_complete_' . $task_id . '_' . $user_id,
253+
$token,
254+
DAY_IN_SECONDS
255+
);
256+
257+
return $token;
258+
}
259+
260+
/**
261+
* Verify a task completion token.
262+
*
263+
* @param string $task_id The task ID.
264+
* @param int $user_id The user ID.
265+
* @param string $provided_token The token to verify.
266+
*
267+
* @return bool True if token is valid, false otherwise.
268+
*/
269+
protected function verify_task_completion_token( $task_id, $user_id, $provided_token ) {
270+
$stored_token = \get_transient( 'prpl_complete_' . $task_id . '_' . $user_id );
271+
272+
if ( ! $stored_token ) {
273+
return false; // Token expired or doesn't exist.
274+
}
275+
276+
// Use hash_equals to prevent timing attacks.
277+
return \hash_equals( $stored_token, $provided_token );
278+
}
279+
280+
/**
281+
* Delete a task completion token after use.
282+
*
283+
* @param string $task_id The task ID.
284+
* @param int $user_id The user ID.
285+
*
286+
* @return bool True if deleted, false otherwise.
287+
*/
288+
protected function delete_task_completion_token( $task_id, $user_id ) {
289+
return \delete_transient( 'prpl_complete_' . $task_id . '_' . $user_id );
290+
}
291+
215292
/**
216293
* Handle the suggested task action.
217294
*

classes/suggested-tasks/providers/class-email-sending.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,15 @@ public function init() {
116116
\add_action( 'init', [ $this, 'check_if_wp_mail_has_override' ], PHP_INT_MAX );
117117

118118
$this->email_subject = \esc_html__( 'Your Progress Planner test message!', 'progress-planner' );
119+
120+
// SECURITY FIX: Generate a secure token for the completion link to prevent CSRF.
121+
$user_id = \get_current_user_id();
122+
$token = \progress_planner()->get_suggested_tasks()->generate_task_completion_token( $this->get_task_id(), $user_id );
123+
119124
$this->email_content = \sprintf(
120125
// translators: %1$s the admin URL.
121126
\__( 'You just used Progress Planner to verify if sending email works on your website. <br><br> The good news; it does! <a href="%1$s" target="_self">Click here to mark %2$s\'s Recommendation as completed</a>.', 'progress-planner' ),
122-
\admin_url( 'admin.php?page=progress-planner&prpl_complete_task=' . $this->get_task_id() ),
127+
\admin_url( 'admin.php?page=progress-planner&prpl_complete_task=' . $this->get_task_id() . '&token=' . $token ),
123128
\esc_html( \progress_planner()->get_ui__branding()->get_ravi_name() )
124129
);
125130
}
@@ -255,6 +260,8 @@ protected function is_there_sending_email_override() {
255260
/**
256261
* Test email sending.
257262
*
263+
* SECURITY FIX: Changed to use check_ajax_referer and get email from $_POST.
264+
*
258265
* @return void
259266
*/
260267
public function ajax_test_email_sending() {
@@ -263,18 +270,32 @@ public function ajax_test_email_sending() {
263270
\wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to test email sending.', 'progress-planner' ) ] );
264271
}
265272

266-
// Check the nonce.
267-
\check_admin_referer( 'progress_planner' );
273+
// SECURITY FIX: Use check_ajax_referer for AJAX handlers.
274+
if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) {
275+
\wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] );
276+
}
268277

269-
$email_address = isset( $_GET['email_address'] ) ? \sanitize_email( \wp_unslash( $_GET['email_address'] ) ) : '';
278+
// SECURITY FIX: Get email from POST data (AJAX request).
279+
$email_address = isset( $_POST['email_address'] ) ? \sanitize_email( \wp_unslash( $_POST['email_address'] ) ) : '';
270280

271281
if ( ! $email_address ) {
272282
\wp_send_json_error( \esc_html__( 'Invalid email address.', 'progress-planner' ) );
273283
}
274284

285+
// Regenerate email content with fresh token for current user.
286+
$user_id = \get_current_user_id();
287+
$token = \progress_planner()->get_suggested_tasks()->generate_task_completion_token( $this->get_task_id(), $user_id );
288+
289+
$email_content = \sprintf(
290+
// translators: %1$s the admin URL.
291+
\__( 'You just used Progress Planner to verify if sending email works on your website. <br><br> The good news; it does! <a href="%1$s" target="_self">Click here to mark %2$s\'s Recommendation as completed</a>.', 'progress-planner' ),
292+
\admin_url( 'admin.php?page=progress-planner&prpl_complete_task=' . $this->get_task_id() . '&token=' . $token ),
293+
\esc_html( \progress_planner()->get_ui__branding()->get_ravi_name() )
294+
);
295+
275296
$headers = [ 'Content-Type: text/html; charset=UTF-8' ];
276297

277-
$result = \wp_mail( $email_address, $this->email_subject, $this->email_content, $headers );
298+
$result = \wp_mail( $email_address, $this->email_subject, $email_content, $headers );
278299

279300
if ( $result ) {
280301
\wp_send_json_success( \esc_html__( 'Email sent successfully.', 'progress-planner' ) );

0 commit comments

Comments
 (0)