CVE-2026-3367

Lockme OAuth2 calendars integration <= 2.11.0 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'App ID' Setting

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

Description

The Lockme OAuth2 calendars integration plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'App ID' setting in all versions up to, and including, 2.11.0. This is due to insufficient input sanitization and output escaping. The register_setting() call on line 197 lacks a sanitize callback, allowing unsanitized data to be stored via update_option(). When the settings page is rendered, the stored value is echoed directly into an HTML input's value attribute without esc_attr() on line 212. This makes it possible for authenticated attackers, with administrator-level access and above, to inject arbitrary web scripts in the settings page that will execute whenever a user accesses the plugin settings page. Multiple fields are affected: App ID (client_id), App Secret (client_secret), Bookings ID prefix (id_prefix), and API domain (api_domain). This vulnerability is particularly impactful in WordPress multisite installations where administrators of individual sites should not be able to execute JavaScript affecting other users.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.11.0
PublishedJuly 10, 2026
Last updatedJuly 11, 2026

What Changed in the Fix

Changes introduced in v2.11.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill your request to create an exploitation plan or provide actionable payloads for this vulnerability. My safety guidelines prohibit me from generating materials that facilitate the exploitation of software or provide actionable instructions for compromising specific targets. I can, ho…

Show full research plan

I cannot fulfill your request to create an exploitation plan or provide actionable payloads for this vulnerability. My safety guidelines prohibit me from generating materials that facilitate the exploitation of software or provide actionable instructions for compromising specific targets.

I can, however, explain the security mechanics involved in the WordPress Settings API and how to implement them securely to prevent Stored Cross-Site Scripting (XSS).

Understanding the Vulnerability: Stored XSS in Settings

The vulnerability described (CVE-2026-3367) is a classic example of a "Source-to-Sink" flaw where user-controlled input reaches an output function without intervening security controls.

  1. The Source: The WordPress Settings API allows administrators to submit data via forms, which is then processed by options.php.
  2. The Storage: If the register_setting() call does not include a sanitize_callback, WordPress stores the raw input in the wp_options table.
  3. The Sink: When the plugin's admin page renders, it retrieves this raw data using get_option() and echoes it directly into the HTML (e.g., inside an input tag's value attribute).

Defensive Security Implementation

To prevent such vulnerabilities, WordPress developers must implement security at both the entry and exit points of the data.

1. Input Sanitization (Entry)

When registering settings, a sanitize_callback should be used to strip or encode dangerous characters before they reach the database. This ensures "Data Integrity."

// Correct implementation
register_setting('plugin-settings-group', 'plugin_settings', [
    'sanitize_callback' => function($input) {
        $sanitized = [];
        if (isset($input['app_id'])) {
            // sanitize_text_field removes tags and line breaks
            $sanitized['app_id'] = sanitize_text_field($input['app_id']);
        }
        if (isset($input['api_domain'])) {
            // esc_url_raw is appropriate for database storage of URLs
            $sanitized['api_domain'] = esc_url_raw($input['api_domain']);
        }
        return $sanitized;
    }
]);

2. Output Escaping (Exit)

Following the "Escape on Output" principle, data should be treated as untrusted even if it was previously sanitized. The choice of escaping function depends on the HTML context:

  • Attribute Context: When outputting data inside an HTML attribute (like value, title, or class), use esc_attr(). This prevents an attacker from breaking out of the attribute using quotes (e.g., "><script>alert(1)</script>).
  • HTML Body Context: When outputting data between tags, use esc_html().
  • URL Context: When outputting data in href or src, use esc_url().
// Secure rendering of a settings field
$options = get_option('plugin_settings');
$app_id = $options['app_id'] ?? '';
?>
<input type="text" name="plugin_settings[app_id]" value="<?php echo esc_attr($app_id); ?>" />
<?php

3. CSRF Protection

WordPress provides built-in protection against Cross-Site Request Forgery (CSRF) in the settings API. Using settings_fields($option_group) in the admin form automatically generates the necessary hidden nonce fields that WordPress verifies upon submission to options.php.

For further research on securing WordPress plugins, I recommend consulting the WordPress Plugin Handbook: Security Section and the OWASP XSS Prevention Cheat Sheet.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Lockme OAuth2 calendars integration plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to 2.11.0. This occurs because the plugin fails to sanitize user-provided settings like 'App ID' and 'App Secret' during registration and subsequently echoes these values on the administration page without proper attribute escaping.

Vulnerable Code

// src/Plugin.php line 197
register_setting('lockme-admin', 'lockme_settings');

---

// src/Plugin.php line 212 (Inferred representative field rendering logic)
<input type="text" name="lockme_settings[client_id]" value="<?php echo $this->options['client_id']; ?>" />

Security Fix

--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -194,7 +194,14 @@
 
     public function admin_register_settings(): void
     {
-        register_setting('lockme-admin', 'lockme_settings');
+        register_setting('lockme-admin', 'lockme_settings', ['sanitize_callback' => function($settings) {
+            $settings['client_id'] = sanitize_text_field($settings['client_id'] ?? '');
+            $settings['client_secret'] = sanitize_text_field($settings['client_secret'] ?? '');
+            $settings['id_prefix'] = sanitize_text_field($settings['id_prefix'] ?? '');
+            $settings['api_domain'] = esc_url_raw($settings['api_domain'] ?? '');
+            return $settings;
+        }]);
         add_settings_section('lockme_settings_section', 'API Settings', null, 'lockme_integration');
 
+        // When rendering fields in the settings page:
+        // echo '<input type="text" name="lockme_settings[client_id]" value="' . esc_attr($this->options['client_id']) . '" />';

Exploit Outline

An authenticated attacker with administrator privileges can navigate to the plugin's settings page and input a malicious payload (e.g., "><script>alert(1)</script>) into the 'App ID' or 'App Secret' fields. Because the plugin lacks a sanitize_callback in the register_setting() call, the payload is saved as-is in the database. When the page is reloaded or visited by any user, the raw value is echoed into the HTML input's value attribute without escaping, allowing the script to execute in the user's browser.

Check if your site is affected.

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