Lockme OAuth2 calendars integration <= 2.11.0 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'App ID' Setting
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:NTechnical Details
<=2.11.0What Changed in the Fix
Changes introduced in v2.11.1
Source Code
WordPress.org SVNI 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.
- The Source: The WordPress Settings API allows administrators to submit data via forms, which is then processed by
options.php. - The Storage: If the
register_setting()call does not include asanitize_callback, WordPress stores the raw input in thewp_optionstable. - 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 aninputtag'svalueattribute).
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, orclass), useesc_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
hreforsrc, useesc_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.
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
@@ -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.