Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/form-notify/form-notify.php
+++ b/form-notify/form-notify.php
@@ -13,7 +13,7 @@
* Plugin Name: FormNotify
* Plugin URI: https://oberonlai.blog/form-notify
* Description: Notification for WordPress form plugins.
- * Version: 1.1.10
+ * Version: 1.1.11
* Author: Daily WPdev.
* Author URI: https://oberonlai.blog
* License: GPL-2.0+
@@ -24,7 +24,7 @@
defined( 'ABSPATH' ) || exit;
-define( 'FORMNOTIFY_VERSION', '1.1.10' );
+define( 'FORMNOTIFY_VERSION', '1.1.11' );
define( 'FORMNOTIFY_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'FORMNOTIFY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'FORMNOTIFY_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@@ -41,7 +41,7 @@
load_plugin_textdomain( 'form-notify', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
-add_action( 'plugin_loaded', 'formnotify_load_plugin_i18n' );
+add_action( 'plugins_loaded', 'formnotify_load_plugin_i18n' );
/**
* Get params from url
@@ -50,11 +50,15 @@
*
* @return string|null
*/
-function formnotify_get_params( string $key ): string|null {
+function formnotify_get_params( string $key ): string {
$query_string = isset( $_SERVER['QUERY_STRING'] ) ? sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : '';
parse_str( $query_string, $params );
- return isset( $params[ $key ] ) ? $params[ $key ] : '';
+ if ( ! isset( $params[ $key ] ) || ! is_scalar( $params[ $key ] ) ) {
+ return '';
+ }
+
+ return sanitize_text_field( (string) $params[ $key ] );
}
/**
--- a/form-notify/src/APIs/HistoryTable.php
+++ b/form-notify/src/APIs/HistoryTable.php
@@ -344,29 +344,41 @@
*/
public function process_bulk_action(): void {
if ( 'delete' === $this->current_action() ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have permission to perform this action.', 'form-notify' ), '', array( 'response' => 403 ) );
+ }
+
$nonce = formnotify_get_params( '_wpnonce' );
$data = formnotify_get_params( 'id' );
if ( ! wp_verify_nonce( $nonce, 'history_delete' ) ) {
- die( '發生錯誤!' );
- } else {
- $this->delete_history( absint( $data ) );
- wp_safe_redirect( admin_url( 'edit.php?post_type=form-notify&page=form-notify-history' ) );
- exit;
+ wp_die( esc_html__( 'Invalid security token.', 'form-notify' ), '', array( 'response' => 403 ) );
}
+
+ $this->delete_history( absint( $data ) );
+ wp_safe_redirect( admin_url( 'edit.php?post_type=form-notify&page=form-notify-history' ) );
+ exit;
}
$action = formnotify_get_params( 'action' );
$action2 = formnotify_get_params( 'action2' );
- $bulk = isset( $_GET['bulk-delete'] ) ? sanitize_text_field( wp_unslash( $_GET['bulk-delete'] ) ) : array();
+ if ( 'bulk-delete' === $action || 'bulk-delete' === $action2 ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have permission to perform this action.', 'form-notify' ), '', array( 'response' => 403 ) );
+ }
- $sanitized_bulk = array_map( 'sanitize_text_field', $bulk );
+ // WP_List_Table generates this nonce via wp_nonce_field( 'bulk-' . $this->_args['plural'] ).
+ check_admin_referer( 'bulk-' . $this->_args['plural'] );
- if ( 'bulk-delete' === $action || 'bulk-delete' === $action2 ) {
- $delete_ids = esc_sql( $sanitized_bulk );
- foreach ( $delete_ids as $id ) {
- $this->delete_history( $id );
+ $bulk = isset( $_GET['bulk-delete'] ) && is_array( $_GET['bulk-delete'] )
+ ? array_map( 'absint', wp_unslash( $_GET['bulk-delete'] ) )
+ : array();
+
+ foreach ( $bulk as $id ) {
+ if ( $id > 0 ) {
+ $this->delete_history( $id );
+ }
}
wp_safe_redirect( admin_url( 'edit.php?post_type=form-notify&page=form-notify-history' ) );
exit;
--- a/form-notify/src/APIs/Line/Login/Button.php
+++ b/form-notify/src/APIs/Line/Login/Button.php
@@ -99,8 +99,8 @@
*/
public function render_button( string $size, string $text, string $align, string $show = null, string $lgmode = 'true' ): string {
if ( ! is_user_logged_in() || 'show' === $show ) {
- // Translators: %s: text.
- return '<div class="form-notify-line-wrap ' . esc_attr( $align ) . '"><a class="size-' . esc_attr( $size ) . '" href="' . esc_attr( get_the_permalink() ) . '?lgmode=' . $lgmode . '"><img src="' . esc_attr( FORMNOTIFY_PLUGIN_URL ) . 'assets/img/icon-line.svg" />' . esc_html( $text ) . '</a></div>';
+ $href = add_query_arg( 'lgmode', rawurlencode( $lgmode ), get_the_permalink() );
+ return '<div class="form-notify-line-wrap ' . esc_attr( $align ) . '"><a class="size-' . esc_attr( $size ) . '" href="' . esc_url( $href ) . '"><img src="' . esc_url( FORMNOTIFY_PLUGIN_URL . 'assets/img/icon-line.svg' ) . '" />' . esc_html( $text ) . '</a></div>';
}
return '';
@@ -130,7 +130,8 @@
$attrs
);
- $r = '<div class="form-notify-line-wrap ' . $param['align'] . '"><a class="size-' . $param['size'] . '" href="' . get_the_permalink() . '?lgmode=' . $param['lgmode'] . '"><img src="' . FORMNOTIFY_PLUGIN_URL . 'assets/img/icon-line.svg">' . esc_html( $param['text'] ) . '</a></div>';
+ $href = add_query_arg( 'lgmode', rawurlencode( $param['lgmode'] ), get_the_permalink() );
+ $r = '<div class="form-notify-line-wrap ' . esc_attr( $param['align'] ) . '"><a class="size-' . esc_attr( $param['size'] ) . '" href="' . esc_url( $href ) . '"><img src="' . esc_url( FORMNOTIFY_PLUGIN_URL . 'assets/img/icon-line.svg' ) . '">' . esc_html( $param['text'] ) . '</a></div>';
return $r;
}
--- a/form-notify/src/APIs/Line/Login/Route.php
+++ b/form-notify/src/APIs/Line/Login/Route.php
@@ -61,15 +61,14 @@
*/
public function get_api_login() {
- $ts = time();
- $state = md5( $ts );
+ $state = wp_generate_password( 32, false );
- set_transient( 'form_notify_line_state_' . $state, $state, 60 * 60 );
+ set_transient( 'form_notify_line_state_' . $state, 1, 60 * 10 );
$line = new SDK();
$url = $line->get_login_url( $state );
- header( 'Location:' . $url );
+ wp_redirect( esc_url_raw( $url ) ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- redirect to LINE OAuth host.
exit;
}
@@ -80,49 +79,52 @@
*/
public function get_api_callback( object $request ): void {
- $line = new SDK();
-
- $code = formnotify_get_params( 'code' );
- $state = formnotify_get_params( 'state' );
- $session_state = get_transient( 'form_notify_line_state_' . $state );
+ $code = formnotify_get_params( 'code' );
+ $state = formnotify_get_params( 'state' );
- if ( empty( $session_state ) ) {
- $session_state = sanitize_text_field( wp_unslash( $_SESSION[ 'form_notify_line_state_' . $state ] ) );
- set_transient( 'form_notify_line_state_' . $state, $state, 60 * 60 );
+ if ( empty( $code ) || empty( $state ) ) {
+ wp_safe_redirect( home_url() );
+ exit;
}
- if ( $session_state !== $state ) {
- $ts = time();
- $state = md5( $ts );
+ $transient_key = 'form_notify_line_state_' . $state;
+ $session_state = get_transient( $transient_key );
- set_transient( 'form_notify_line_state_' . $state, $state, 60 * 60 );
- wp_safe_redirect( $line->get_login_url( $state ) );
+ if ( empty( $session_state ) ) {
+ wp_safe_redirect( home_url() );
exit;
-
}
+ // Single-use state token.
+ delete_transient( $transient_key );
+
+ $line = new SDK();
$token = $line->get_access_token( $code );
- setcookie( 'access_token', $token['access_token'], time() + 3600 * 24 * 14 );
+ if ( empty( $token['access_token'] ) || empty( $token['id_token'] ) ) {
+ wp_safe_redirect( home_url() );
+ exit;
+ }
$user = $line->get_line_profile( $token['access_token'], $token['id_token'] );
- if ( $user ) {
-
- $user_raw_id = $user->sub;
- $user_display = $user->name;
- $user_avatar = $user->picture;
- $has_real_email = ! empty( $user->email );
- $user_email = $has_real_email ? $user->email : $user_raw_id . '@line.com';
-
- $user_obj = new User();
- if ( $user_obj->is_member( $user_email, $user_avatar ) ) {
- $user_obj->login( $user_raw_id, $user_email, $user_display, $user_avatar, $has_real_email );
- } else {
- $user_obj->sign_up( $user_raw_id, $user_email, $user_display, $user_avatar, $has_real_email );
- }
+ if ( ! $user || empty( $user->sub ) ) {
+ wp_safe_redirect( home_url() );
+ exit;
}
+ $user_raw_id = $user->sub;
+ $user_display = isset( $user->name ) ? $user->name : '';
+ $user_avatar = isset( $user->picture ) ? $user->picture : '';
+ $has_real_email = ! empty( $user->email );
+ $user_email = $has_real_email ? $user->email : $user_raw_id . '@line.local';
+
+ $user_obj = new User();
+ if ( $user_obj->is_member( $user_raw_id ) ) {
+ $user_obj->login( $user_raw_id, $user_email, $user_display, $user_avatar, $has_real_email );
+ } else {
+ $user_obj->sign_up( $user_raw_id, $user_email, $user_display, $user_avatar, $has_real_email );
+ }
}
}
--- a/form-notify/src/APIs/Line/Login/Sdk.php
+++ b/form-notify/src/APIs/Line/Login/Sdk.php
@@ -24,13 +24,12 @@
'response_type' => 'code',
'client_id' => get_option( 'form_notify_line_login_channel_id' ),
'state' => $state,
+ 'scope' => 'email openid profile',
+ 'redirect_uri' => rest_url( 'form-notify/v1/callback' ),
+ 'bot_prompt' => 'aggressive',
);
- $host = 'https://access.line.me/oauth2/v2.1/authorize';
-
- $url = $host . '?' . http_build_query( $parameter ) . '&scope=email%20openid%20profile&redirect_uri=' . home_url() . '/wp-json/form-notify/v1/callback&bot_prompt=aggressive';
-
- return $url;
+ return 'https://access.line.me/oauth2/v2.1/authorize?' . http_build_query( $parameter );
}
/**
@@ -44,7 +43,7 @@
$body = array(
'grant_type' => 'authorization_code',
'code' => $code,
- 'redirect_uri' => home_url() . '/wp-json/form-notify/v1/callback',
+ 'redirect_uri' => rest_url( 'form-notify/v1/callback' ),
'client_id' => get_option( 'form_notify_line_login_channel_id' ),
'client_secret' => get_option( 'form_notify_line_login_channel_secret' ),
);
--- a/form-notify/src/APIs/Line/Login/User.php
+++ b/form-notify/src/APIs/Line/Login/User.php
@@ -17,16 +17,16 @@
/**
* User
*
- * @var object $user User.
+ * @var WP_User|false $user User.
*/
- private object|bool $user;
+ private $user = false;
/**
* Roles
*
* @var array $roles Roles.
*/
- private array $roles;
+ private array $roles = array();
/**
* Register
@@ -42,60 +42,75 @@
/**
* Check is member
*
- * @param string $user_email User email.
- * @param string $user_avatar User avatar.
+ * Look up the WordPress user by the LINE sub stored in user_meta.
+ * Never trust client-supplied email for identity.
+ *
+ * @param string $user_raw_id LINE user sub.
*
* @return bool
*/
- public function is_member( string $user_email, string $user_avatar ): bool {
- $this->user = get_user_by( 'email', $user_email );
- $this->roles[] = $this->user->roles;
- if ( ! is_wp_error( $this->user ) && $this->user ) {
- return true;
+ public function is_member( string $user_raw_id ): bool {
+ if ( empty( $user_raw_id ) ) {
+ return false;
+ }
+
+ $query = new WP_User_Query(
+ array(
+ 'meta_key' => 'form_notify_line_user_id',
+ 'meta_value' => $user_raw_id,
+ 'number' => 1,
+ 'fields' => 'all',
+ )
+ );
+
+ $results = $query->get_results();
+ if ( empty( $results ) ) {
+ return false;
}
- return false;
+ $this->user = $results[0];
+ $this->roles = (array) $this->user->roles;
+
+ return true;
}
/**
* Login
*
- * @param string $user_raw_id User raw id.
- * @param string $user_email User email.
- * @param string $user_display User display.
+ * @param string $user_raw_id User raw id.
+ * @param string $user_email User email.
+ * @param string $user_display User display.
* @param string $user_avatar User avatar.
* @param bool $has_real_email Whether LINE provided a real email.
*
* @return void
*/
public function login( string $user_raw_id, string $user_email, string $user_display, string $user_avatar, bool $has_real_email = false ): void {
- if ( ! is_user_logged_in() ) {
-
- wp_clear_auth_cookie();
- wp_set_current_user( $this->user->ID );
- wp_set_auth_cookie( $this->user->ID, true, is_ssl() );
-
- if ( ! get_user_meta( $this->user->ID, 'form_notify_line_user_id', true ) ) {
- update_user_meta( $this->user->ID, 'form_notify_line_user_id', $user_raw_id );
- update_user_meta( $this->user->ID, 'form_notify_line_user_avatar', $user_avatar );
- update_user_meta( $this->user->ID, 'nickname', $user_display );
- if ( $has_real_email ) {
- update_user_meta( $this->user->ID, 'billing_email', $user_email );
- }
- }
+ if ( is_user_logged_in() || ! $this->user ) {
+ return;
+ }
- $this->roles = $this->user->roles;
- $this->set_logged_redirect( 'login' );
+ wp_clear_auth_cookie();
+ wp_set_current_user( $this->user->ID );
+ wp_set_auth_cookie( $this->user->ID, true );
+ update_user_meta( $this->user->ID, 'form_notify_line_user_avatar', $user_avatar );
+ if ( ! get_user_meta( $this->user->ID, 'nickname', true ) ) {
+ update_user_meta( $this->user->ID, 'nickname', $user_display );
+ }
+ if ( $has_real_email && ! get_user_meta( $this->user->ID, 'billing_email', true ) ) {
+ update_user_meta( $this->user->ID, 'billing_email', $user_email );
}
+
+ $this->set_logged_redirect( 'login' );
}
/**
* Sign up
*
- * @param string $user_raw_id User raw id.
- * @param string $user_email User email.
- * @param string $user_display User display.
+ * @param string $user_raw_id User raw id.
+ * @param string $user_email User email.
+ * @param string $user_display User display.
* @param string $user_avatar User avatar.
* @param bool $has_real_email Whether LINE provided a real email.
*
@@ -103,42 +118,62 @@
*/
public function sign_up( string $user_raw_id, string $user_email, string $user_display, string $user_avatar, bool $has_real_email = false ): void {
- if ( ! is_user_logged_in() ) {
-
- if ( username_exists( strstr( $user_email, '@', true ) ) ) {
- $user_login = strstr( $user_email, '@', true ) . '-' . wp_rand( 1, 10 );
- } else {
- $user_login = strstr( $user_email, '@', true );
- }
+ if ( is_user_logged_in() ) {
+ return;
+ }
- $userdata = array(
- 'user_login' => $user_login,
- 'user_pass' => $user_email,
- 'user_email' => $user_email,
- 'display_name' => $user_display,
- 'nickname' => $user_display,
- 'role' => $this->role_check(),
- );
+ // If LINE provided a real email and that email already belongs to a WP account,
+ // do not auto-link or auto-create — abort to prevent account hijack.
+ if ( $has_real_email && email_exists( $user_email ) ) {
+ wp_safe_redirect( home_url() );
+ exit;
+ }
- $user_id = wp_insert_user( $userdata );
+ $base_login = sanitize_user( strstr( $user_email, '@', true ), true );
+ if ( empty( $base_login ) ) {
+ $base_login = 'line_' . substr( md5( $user_raw_id ), 0, 8 );
+ }
+ $user_login = $base_login;
+ $suffix = 1;
+ while ( username_exists( $user_login ) ) {
+ $user_login = $base_login . '-' . $suffix;
+ ++$suffix;
+ }
+
+ $userdata = array(
+ 'user_login' => $user_login,
+ 'user_pass' => wp_generate_password( 32, true, true ),
+ 'user_email' => $user_email,
+ 'display_name' => $user_display,
+ 'nickname' => $user_display,
+ 'role' => $this->role_check(),
+ );
- update_user_meta( $user_id, 'form_notify_line_user_id', $user_raw_id );
- update_user_meta( $user_id, 'form_notify_line_user_avatar', $user_avatar );
- if ( $has_real_email ) {
- update_user_meta( $user_id, 'billing_email', $user_email );
- }
+ $user_id = wp_insert_user( $userdata );
- if ( function_exists( 'add_user_to_blog' ) ) {
- add_user_to_blog( get_current_blog_id(), $user_id, $this->role_check() );
- }
+ if ( is_wp_error( $user_id ) ) {
+ wp_safe_redirect( home_url() );
+ exit;
+ }
- wp_clear_auth_cookie();
- wp_set_current_user( $user_id );
- wp_set_auth_cookie( $user_id, true, is_ssl() );
+ update_user_meta( $user_id, 'form_notify_line_user_id', $user_raw_id );
+ update_user_meta( $user_id, 'form_notify_line_user_avatar', $user_avatar );
+ if ( $has_real_email ) {
+ update_user_meta( $user_id, 'billing_email', $user_email );
+ }
- $this->set_logged_redirect( 'signup' );
+ if ( function_exists( 'add_user_to_blog' ) ) {
+ add_user_to_blog( get_current_blog_id(), $user_id, $this->role_check() );
}
+ wp_clear_auth_cookie();
+ wp_set_current_user( $user_id );
+ wp_set_auth_cookie( $user_id, true );
+
+ $this->user = get_user_by( 'id', $user_id );
+ $this->roles = $this->user ? (array) $this->user->roles : array();
+
+ $this->set_logged_redirect( 'signup' );
}
/**
@@ -147,15 +182,14 @@
* @return string
*/
private function role_check(): string {
- if ( get_option( 'form_notify_line_btn_user_role' ) ) {
- return get_option( 'form_notify_line_btn_user_role' );
- } else {
- if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) {
- return 'customer';
- } else {
- return 'subscriber';
- }
+ $configured = get_option( 'form_notify_line_btn_user_role' );
+ if ( $configured ) {
+ return (string) $configured;
+ }
+ if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) {
+ return 'customer';
}
+ return 'subscriber';
}
/**
@@ -164,31 +198,46 @@
public function set_login_redirect_url(): void {
$lgmode = formnotify_get_params( 'lgmode' );
- if ( $lgmode ) {
- session_start();
+ if ( ! $lgmode ) {
+ return;
+ }
- $line = new SDK();
- $state = md5( time() );
+ $line = new SDK();
+ $state = wp_generate_password( 32, false );
- $redirect_url = '';
+ $redirect_url = '';
- if ( 'true' === $lgmode ) {
- if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
- $http_post = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
- $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
- $redirect_url = preg_replace( '~(?|&)lgmode=[^&]*~', '$1', 'https://' . $http_post . $request_uri );
- }
- } elseif ( str_contains( $lgmode, 'http' ) ) {
- $redirect_url = wp_unslash( $lgmode );
+ if ( 'true' === $lgmode ) {
+ if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
+ $http_host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );
+ $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
+ $scheme = is_ssl() ? 'https://' : 'http://';
+ $redirect_url = preg_replace( '~(?|&)lgmode=[^&]*~', '$1', $scheme . $http_host . $request_uri );
+ $redirect_url = wp_validate_redirect( $redirect_url, home_url() );
}
+ } elseif ( str_contains( $lgmode, 'http' ) ) {
+ // Only allow on-site redirects.
+ $redirect_url = wp_validate_redirect( $lgmode, home_url() );
+ }
+
+ if ( $redirect_url ) {
+ setcookie(
+ 'form_notify_login_redirect',
+ esc_url_raw( $redirect_url ),
+ array(
+ 'expires' => time() + 3600,
+ 'path' => '/',
+ 'secure' => is_ssl(),
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ )
+ );
+ }
- setcookie( 'login_redirect_url', $redirect_url, time() + 3600, '/' );
- $_SESSION[ 'form_notify_line_state_' . $state ] = $state;
- set_transient( 'form_notify_line_state_' . $state, $state, 60 * 60 );
+ set_transient( 'form_notify_line_state_' . $state, 1, 60 * 10 );
- header( 'Location:' . $line->get_login_url( $state ) );
- exit;
- }
+ wp_redirect( esc_url_raw( $line->get_login_url( $state ) ) ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- redirect to LINE OAuth host.
+ exit;
}
/**
@@ -198,18 +247,42 @@
*/
public function set_logged_redirect( string $type ): void {
- $login_redirect_url = isset( $_COOKIE['login_redirect_url'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['login_redirect_url'] ) ) : '';
- $admin_roles = array( 'administrator', 'shop_manager' );
- $is_admin = ( count( array_intersect( $admin_roles, $this->roles ) ) > 0 ) ? true : false;
- $is_wp_login = str_contains( $login_redirect_url, 'wp-login.php' );
+ $cookie_redirect = isset( $_COOKIE['form_notify_login_redirect'] )
+ ? esc_url_raw( wp_unslash( $_COOKIE['form_notify_login_redirect'] ) )
+ : '';
+ // Validate cookie value is on-site only.
+ $login_redirect_url = $cookie_redirect ? wp_validate_redirect( $cookie_redirect, '' ) : '';
+
+ $option_redirect = get_option( 'form_notify_line_btn_redirect' );
+ // Validate option value is on-site (admin-set, but defensive).
+ $option_redirect = $option_redirect ? wp_validate_redirect( $option_redirect, '' ) : '';
+
+ $admin_roles = array( 'administrator', 'shop_manager' );
+ $is_admin = ( count( array_intersect( $admin_roles, $this->roles ) ) > 0 );
+ $is_wp_login = $login_redirect_url && str_contains( $login_redirect_url, 'wp-login.php' );
+
+ // Clear the redirect cookie after consumption.
+ if ( $cookie_redirect ) {
+ setcookie(
+ 'form_notify_login_redirect',
+ '',
+ array(
+ 'expires' => time() - 3600,
+ 'path' => '/',
+ 'secure' => is_ssl(),
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ )
+ );
+ }
- if ( $login_redirect_url && ! get_option( 'form_notify_line_btn_redirect' ) ) {
- header( 'Location:' . $login_redirect_url );
+ if ( $login_redirect_url && ! $option_redirect ) {
+ wp_safe_redirect( $login_redirect_url );
exit;
}
- if ( get_option( 'form_notify_line_btn_redirect' ) ) {
- header( 'Location:' . get_option( 'form_notify_line_btn_redirect' ) );
+ if ( $option_redirect ) {
+ wp_safe_redirect( $option_redirect );
exit;
}
@@ -257,34 +330,30 @@
*/
public function replace_avatar_url( array $args, mixed $id_or_email ): array {
- $user_id = '';
-
- if ( 'object' === gettype( $id_or_email ) && 'WP_Comment' === get_class( $id_or_email ) ) {
- $user_id = $id_or_email->user_id;
- }
-
- if ( 'object' === gettype( $id_or_email ) && 'WP_User' === get_class( $id_or_email ) ) {
- $user_id = $id_or_email->ID;
- }
+ $user_id = 0;
- if ( 'integer' === gettype( $id_or_email ) ) {
+ if ( $id_or_email instanceof WP_Comment ) {
+ $user_id = (int) $id_or_email->user_id;
+ } elseif ( $id_or_email instanceof WP_User ) {
+ $user_id = (int) $id_or_email->ID;
+ } elseif ( is_int( $id_or_email ) ) {
$user_id = $id_or_email;
- }
-
- if ( 'string' === gettype( $id_or_email ) && strpos( $id_or_email, '@' ) !== false ) {
- $user_id = get_user_by( 'email', $id_or_email )->ID;
+ } elseif ( is_string( $id_or_email ) && strpos( $id_or_email, '@' ) !== false ) {
+ $user = get_user_by( 'email', $id_or_email );
+ if ( $user ) {
+ $user_id = (int) $user->ID;
+ }
}
if ( $user_id ) {
- if ( get_user_meta( $user_id, 'form_notify_line_user_avatar' ) ) {
- $args['url'] = get_user_meta( $user_id, 'form_notify_line_user_avatar', true );
+ $avatar = get_user_meta( $user_id, 'form_notify_line_user_avatar', true );
+ if ( $avatar ) {
+ $args['url'] = esc_url_raw( $avatar );
}
}
return $args;
-
}
-
}
User::register();
--- a/form-notify/src/APIs/Line/Message.php
+++ b/form-notify/src/APIs/Line/Message.php
@@ -31,7 +31,7 @@
* Construct
*/
public function __construct() {
- $this->token = ( get_option( 'form_notify_line_message_token' ) ) ? get_option( 'form_notify_line_message_token' ) : 'xrTVdDn+qvmS/vl1wicjOt9zsonq1fquP78yb/EOAlXIR+BwmxQd11a5kJLPN3vE4eN0KYgbXook7qAreUVWm9JFBSulgU1UKpvvQgaHNjqMYoHSi1UCVvWzGWGkXSbAIl/o2M+mlibm9xpW4nW32AdB04t89/1O/w1cDnyilFU=';
+ $this->token = ( get_option( 'form_notify_line_message_token' ) ) ? get_option( 'form_notify_line_message_token' ) : '';
$this->endpoint = 'https://api.line.me/v2/bot/message/push';
}
--- a/form-notify/src/APIs/Metabox/Metabox.php
+++ b/form-notify/src/APIs/Metabox/Metabox.php
@@ -152,14 +152,13 @@
foreach ( $this->fields as $field ) {
if ( isset( $_POST[ $field['id'] ] ) ) {
- $post_field_id = $this->sanitize_recursive( $_POST[ $field['id'] ] );
+ $raw = wp_unslash( $_POST[ $field['id'] ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below.
+ $post_field_id = $this->sanitize_recursive( $raw );
- if ( 'text' === $field['type'] || 'textarea' === $field['type'] ) {
- update_post_meta( $post->ID, $field['id'], $post_field_id );
- } elseif ( 'multiselect' === $field['type'] ) {
+ if ( 'multiselect' === $field['type'] ) {
update_post_meta( $post->ID, $field['id'], wp_json_encode( $post_field_id ) );
} else {
- update_post_meta( $post->ID, $field['id'], $post_field_id ); // phpcs:ignore
+ update_post_meta( $post->ID, $field['id'], $post_field_id );
}
} else {
delete_post_meta( $post->ID, $field['id'] );
@@ -167,16 +166,26 @@
}
}
+ /**
+ * Recursively sanitize a value (scalar or nested array).
+ *
+ * @param mixed $data Raw value.
+ *
+ * @return mixed
+ */
public function sanitize_recursive( $data ) {
if ( is_array( $data ) ) {
+ $out = array();
foreach ( $data as $key => $value ) {
- $data[ $key ] = $value;
+ $safe_key = is_string( $key ) ? sanitize_key( $key ) : $key;
+ $out[ $safe_key ] = $this->sanitize_recursive( $value );
}
- } else {
- $data = sanitize_text_field( $data );
+ return $out;
}
-
- return $data;
+ if ( is_scalar( $data ) ) {
+ return sanitize_textarea_field( (string) $data );
+ }
+ return '';
}
/**
--- a/form-notify/src/APIs/Sms/Easygo.php
+++ b/form-notify/src/APIs/Sms/Easygo.php
@@ -161,7 +161,7 @@
'methods' => 'GET',
'callback' => array( $this, 'get_points_api_body' ),
'permission_callback' => function () {
- return true;
+ return current_user_can( 'manage_options' );
},
)
);
--- a/form-notify/src/APIs/Sms/Every8d.php
+++ b/form-notify/src/APIs/Sms/Every8d.php
@@ -266,7 +266,7 @@
'methods' => 'GET',
'callback' => array( $this, 'get_points_api_body' ),
'permission_callback' => function () {
- return true;
+ return current_user_can( 'manage_options' );
},
)
);
--- a/form-notify/src/APIs/Sms/Mitake.php
+++ b/form-notify/src/APIs/Sms/Mitake.php
@@ -151,7 +151,7 @@
'methods' => 'GET',
'callback' => array( $this, 'get_points_api_body' ),
'permission_callback' => function () {
- return true;
+ return current_user_can( 'manage_options' );
},
)
);
@@ -165,11 +165,13 @@
* @return string
*/
public function get_callback_body( WP_REST_Request $request ): string {
- $msgid = $request['msgid'];
- $phone = $request['dstaddr'];
- $code = $request['statuscode'];
- $statusstr = $request['statusstr'];
- $statusflag = $request['StatusFlag'];
+ $msgid = is_string( $request['msgid'] ?? null ) ? sanitize_text_field( $request['msgid'] ) : '';
+ $statusstr = is_string( $request['statusstr'] ?? null ) ? sanitize_text_field( $request['statusstr'] ) : '';
+
+ if ( empty( $msgid ) ) {
+ return '';
+ }
+
$history_id = History::select( $msgid, 'notify_type' );
$status = match ( $statusstr ) {
--- a/form-notify/src/Events/AbstractNotify.php
+++ b/form-notify/src/Events/AbstractNotify.php
@@ -173,12 +173,21 @@
break;
case 'email':
- $subject = $this->replace_message_content( $action['form_notify_action_module_subject'], $form_data );
- $headers = array( 'Content-Type: text/html; charset=UTF-8' );
- $message = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width, initial-scale=1.0"><title></title><!--[if (gte mso 9)|(IE)]><style type="text/css">table{border-collapse: collapse;}</style><![endif]--><style type="text/css"> body{margin: 0 !important; padding: 0; background-color: #ffffff; font-family: "HanHei TC", "PingFang TC", "Helvetica Neue", "Helvetica", "STHeitiTC-Light", "Arial", sans-serif;}table{width:100%;border-spacing: 0; color: #4A4A4A;}td{padding: 0;}img{border: 0;}ul{margin: 0; padding: 0;}li{list-style: none; font-size: 14px; color: #4A4A4A; margin-bottom: 20px;}div[style*="margin: 16px 0"]{margin:0 !important;}.wrapper{width: 100%; table-layout: fixed; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;}.webkit{max-width: 680px; width: 90%;}.inner{padding: 37px;}p{Margin: 0;}a{color: DodgerBlue; text-decoration: none;}.one-column .contents{text-align: left;}.one-column p{font-size: 14px; margin-bottom: 10px;}.content{background: #efefef;}.color{color: #FA666C;}.btn{position: relative; width: 100%; margin: 10px 0 0 0; display: inline-block; padding: 1rem 0; text-align: center; font-size: 1.2rem; white-space: nowrap;-webkit-border-radius: 100px;-moz-border-radius: 100px;-ms-border-radius: 100px;-o-border-radius: 100px;border-radius: 100px;-webkit-transition: all .2s ease;-o-transition: all .2s ease;transition: all .2s ease;}.btnorage{background: #ED6B00; color: #fff;}.btnorage:hover,.btnred:active{background: #F09F54;}.hr{display: block; width: 100%; height: 1px; margin: 20px 0; background: #ccc;}</style></head><body> <div class="wrapper"> <div class="webkit"><!--[if (gte mso 9)|(IE)]> <table width="375" align="center"> <tr> <td><![endif]--> <table class="outer" align="center"> <tbody> <tr class="psingle"> <td class="one-column"> <table width="100%"> <tbody> <tr> </tr></tbody> </table> </td></tr><tr class="psingle content"> <td class="one-column"> <table width="100%"> <tbody> <tr> <td class="inner contents"> <h3 style="text-align:center">' . $this->replace_message_content( $action['form_notify_action_module_subject'], $form_data ) . '</h3><div>' . nl2br( $this->replace_message_content( $action['form_notify_action_module_content'], $form_data ) ) . '</div><br><hr> <br><p style="text-align: center;">' . get_bloginfo( 'name' ) . '</p><p style="text-align: center;"><a href="' . home_url() . '">' . home_url() . '</a></p></td></tr></tbody> </table> </td></tr></tbody> </table><!--[if (gte mso 9)|(IE)]> </td></tr></table><![endif]--> </div></div> </body></html>';
+ if ( ! is_email( $receiver ) ) {
+ History::insert( 0, $receiver . ' - ' . $this->history_text, 0, __( 'Email', 'form-notify' ), '', __( 'Invalid email address.', 'form-notify' ) );
+ break;
+ }
+ $subject_raw = $this->replace_message_content( $action['form_notify_action_module_subject'], $form_data );
+ $content_raw = $this->replace_message_content( $action['form_notify_action_module_content'], $form_data );
+ $subject = sanitize_text_field( $subject_raw );
+ $content = wp_kses_post( $content_raw );
+ $site_name = esc_html( get_bloginfo( 'name' ) );
+ $site_url = esc_url( home_url() );
+ $headers = array( 'Content-Type: text/html; charset=UTF-8' );
+ $message = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width, initial-scale=1.0"><title></title><!--[if (gte mso 9)|(IE)]><style type="text/css">table{border-collapse: collapse;}</style><![endif]--><style type="text/css"> body{margin: 0 !important; padding: 0; background-color: #ffffff; font-family: "HanHei TC", "PingFang TC", "Helvetica Neue", "Helvetica", "STHeitiTC-Light", "Arial", sans-serif;}table{width:100%;border-spacing: 0; color: #4A4A4A;}td{padding: 0;}img{border: 0;}ul{margin: 0; padding: 0;}li{list-style: none; font-size: 14px; color: #4A4A4A; margin-bottom: 20px;}div[style*="margin: 16px 0"]{margin:0 !important;}.wrapper{width: 100%; table-layout: fixed; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;}.webkit{max-width: 680px; width: 90%;}.inner{padding: 37px;}p{Margin: 0;}a{color: DodgerBlue; text-decoration: none;}.one-column .contents{text-align: left;}.one-column p{font-size: 14px; margin-bottom: 10px;}.content{background: #efefef;}.color{color: #FA666C;}.btn{position: relative; width: 100%; margin: 10px 0 0 0; display: inline-block; padding: 1rem 0; text-align: center; font-size: 1.2rem; white-space: nowrap;-webkit-border-radius: 100px;-moz-border-radius: 100px;-ms-border-radius: 100px;-o-border-radius: 100px;border-radius: 100px;-webkit-transition: all .2s ease;-o-transition: all .2s ease;transition: all .2s ease;}.btnorage{background: #ED6B00; color: #fff;}.btnorage:hover,.btnred:active{background: #F09F54;}.hr{display: block; width: 100%; height: 1px; margin: 20px 0; background: #ccc;}</style></head><body> <div class="wrapper"> <div class="webkit"><!--[if (gte mso 9)|(IE)]> <table width="375" align="center"> <tr> <td><![endif]--> <table class="outer" align="center"> <tbody> <tr class="psingle"> <td class="one-column"> <table width="100%"> <tbody> <tr> </tr></tbody> </table> </td></tr><tr class="psingle content"> <td class="one-column"> <table width="100%"> <tbody> <tr> <td class="inner contents"> <h3 style="text-align:center">' . esc_html( $subject ) . '</h3><div>' . nl2br( $content ) . '</div><br><hr> <br><p style="text-align: center;">' . $site_name . '</p><p style="text-align: center;"><a href="' . $site_url . '">' . $site_url . '</a></p></td></tr></tbody> </table> </td></tr></tbody> </table><!--[if (gte mso 9)|(IE)]> </td></tr></table><![endif]--> </div></div> </body></html>';
wp_mail( $receiver, $subject, $message, $headers );
- History::insert( 0, $receiver . ' - ' . $this->history_text, 0, __( 'Email', 'form-notify' ), $this->replace_message_content( $action['form_notify_action_module_content'], $form_data ), __( 'Success', 'form-notify' ) );
+ History::insert( 0, $receiver . ' - ' . $this->history_text, 0, __( 'Email', 'form-notify' ), $content_raw, __( 'Success', 'form-notify' ) );
break;
default:
// code...
@@ -227,16 +236,16 @@
// Nextend Social Login.
global $wpdb;
- $sql = $wpdb->prepare( "SELECT identifier FROM `{$wpdb->prefix}social_users` WHERE `ID` = %d ", $user_id );
+ $cache_key = 'form_notify_identifier_' . $user_id;
+ $cache = wp_cache_get( $cache_key );
- $cache = wp_cache_get( 'form_notify_identifier' );
-
- if ( ! $cache ) {
- // @codingStandardsIgnoreStart
- $result = $wpdb->get_results( $sql );
- $cache = $result[0]->identifier;
- wp_cache_set( 'form_notify_identifier', $cache );
- // @codingStandardsIgnoreEnd
+ if ( false === $cache ) {
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- caching handled below.
+ $result = $wpdb->get_var(
+ $wpdb->prepare( "SELECT identifier FROM `{$wpdb->prefix}social_users` WHERE `ID` = %d", $user_id )
+ );
+ $cache = $result ? $result : '';
+ wp_cache_set( $cache_key, $cache );
}
return $cache;
--- a/form-notify/src/Options/History.php
+++ b/form-notify/src/Options/History.php
@@ -53,8 +53,9 @@
<div id="post-body" class="metabox-holder">
<div id="post-body-content">
<div class="meta-box-sortables ui-sortable">
- <form method="get" action="<?php echo esc_html( admin_url() ); ?>admin.php?edit_php?post_type=form-notify&page=form-notify-history">
- <input type="hidden" name="page" value="form-notify-history"/>
+ <form method="get" action="<?php echo esc_url( admin_url( 'edit.php' ) ); ?>">
+ <input type="hidden" name="post_type" value="form-notify"/>
+ <input type="hidden" name="page" value="form-notify-history"/>
<?php
$this->list_obj->views();
$this->list_obj->prepare_items();
@@ -192,22 +193,22 @@
*/
public static function select( string $keyword, string $field ): int {
global $wpdb;
+
+ $allowed_fields = array( 'user_info', 'notify_type', 'notify_content', 'status' );
+ if ( ! in_array( $field, $allowed_fields, true ) ) {
+ return 0;
+ }
+
$table_name = $wpdb->prefix . 'form_notify_history';
- $results = $wpdb->get_results(
- $wpdb->prepare(
- 'SELECT ID FROM %s WHERE %s LIKE %s',
- $table_name,
- $field,
- '%' . $wpdb->esc_like( $keyword ) . '%'
- )
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $field is from allow-list above; $table_name is server-side.
+ $sql = $wpdb->prepare(
+ "SELECT id FROM {$table_name} WHERE {$field} LIKE %s LIMIT 1",
+ '%' . $wpdb->esc_like( $keyword ) . '%'
);
- if ( $results ) {
- foreach ( $results as $result ) {
- return $result->ID;
- }
- }
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot lookup.
+ $row = $wpdb->get_row( $sql );
- return 0;
+ return $row ? (int) $row->id : 0;
}
}