Greenhouse Job Board <= 2.7.3 - Authenticated (Administrator+) Stored Cross-Site Scripting
Description
The Greenhouse Job Board plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 2.7.3 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=2.7.3This research plan outlines the technical steps to analyze and exploit **CVE-2025-67633**, a Stored Cross-Site Scripting (XSS) vulnerability in the **Greenhouse Job Board** plugin. ### 1. Vulnerability Summary * **Vulnerability:** Authenticated Stored XSS. * **Vulnerable Component:** Plugin set…
Show full research plan
This research plan outlines the technical steps to analyze and exploit CVE-2025-67633, a Stored Cross-Site Scripting (XSS) vulnerability in the Greenhouse Job Board plugin.
1. Vulnerability Summary
- Vulnerability: Authenticated Stored XSS.
- Vulnerable Component: Plugin settings page or custom post type metadata handled by the plugin.
- Cause: The plugin fails to sanitize user-supplied input when saving settings and fails to escape that data when rendering it in the WordPress admin dashboard or on the frontend.
- Constraint: Specifically impacts environments where
unfiltered_htmlis disabled (e.g., Multi-site ordefine( 'DISALLOW_UNFILTERED_HTML', true );). In these environments, even Administrators are subjected toksesfiltering, but this plugin likely bypasses or omits those checks in its custom implementation.
2. Attack Vector Analysis
- Endpoint: WordPress Admin Settings page.
- Authentication: Administrator level or higher.
- Payload Location: Plugin configuration options (e.g., Greenhouse Account ID, API Key, or Display Settings).
- Precondition: The site must be a Multi-site installation OR
unfiltered_htmlmust be explicitly disabled for the Administrator role.
3. Code Flow (Inferred)
- Registration: The plugin registers a settings page using
add_options_page()oradd_menu_page()(likely in a class handling admin menus). - Input Processing: Settings are registered via
register_setting(). If asanitize_callbackis missing or uses a weak function (liketrim), malicious scripts can be stored in thewp_optionstable. - Persistence: The payload is saved via
update_option()when the admin submits the settings form. - Sink (Rendering): The stored option is retrieved using
get_option()and echoed directly into an HTML attribute (e.g.,value="<?php echo get_option('ghjb_id'); ?>") or a container without usingesc_attr()oresc_html().
4. Nonce Acquisition Strategy
Since the exploit requires Administrator access to modify settings, the agent must authenticate and extract the settings nonce.
- Identify Page: Navigate to the plugin settings page (likely
/wp-admin/admin.php?page=greenhouse-job-board). - Locate Form: Find the
<form>that handles the settings. - Extract Nonce:
- WordPress uses the Settings API. The nonce field name is typically
_wpnonce. - Action:
browser_navigateto the settings page. - Action:
browser_evalto get the nonce:document.querySelector('input[name="_wpnonce"]').value. - Also, extract the
option_pageandactionvalues (usuallyupdate).
- WordPress uses the Settings API. The nonce field name is typically
5. Exploitation Strategy
The exploitation involves updating a plugin setting with a payload that breaks out of an HTML context.
Step 1: Initial Discovery
Search for the settings registration to identify vulnerable parameters:
grep -r "register_setting" /var/www/html/wp-content/plugins/greenhouse-job-board/
grep -r "get_option" /var/www/html/wp-content/plugins/greenhouse-job-board/ | grep "echo"
Step 2: Payload Construction
If the payload is reflected in an input field value attribute:"><script>alert(document.domain)</script>
Step 3: HTTP Request (using http_request)
- URL:
https://[target]/wp-admin/options.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Body Parameters:
option_page: (Extracted from form, e.g.,greenhouse_job_board_settings)action:update_wpnonce: (The extracted nonce)greenhouse_id(Inferred parameter):"><script>alert(document.domain)</script>
6. Test Data Setup
To accurately reproduce the "vulnerable" state where unfiltered_html is disabled:
- Install Plugin:
wp plugin install greenhouse-job-board --version=2.7.3 --activate - Create Admin:
wp user create exploit_admin admin@example.com --role=administrator --user_pass=password123 - Restrict HTML: Add
define( 'DISALLOW_UNFILTERED_HTML', true );towp-config.php. This forces the Administrator to rely on the plugin's (absent) sanitization. - Confirm Settings Page: Identify the menu slug using
wp admin-menu list.
7. Expected Results
- The
options.phprequest should return a302 Redirectback to the settings page with?settings-updated=true. - Upon visiting the settings page or the frontend job board (depending on where the option is used), the JavaScript payload
alert(document.domain)will execute in the browser context.
8. Verification Steps
After performing the exploit via HTTP, verify the database state using WP-CLI:
# Check if the payload is stored raw in the database
wp option get greenhouse_id --allow-root
# Verify the presence of the payload in the rendered HTML of the admin page
# (Simulated via CLI)
wp eval 'echo get_option("greenhouse_id");' | grep "script"
9. Alternative Approaches
If the main settings page is properly sanitized, check for:
- Shortcode Attributes: If the plugin uses a shortcode like
[greenhouse], check if attributes are echoed without escaping.- Setup: Create a post with
[greenhouse id='"><script>alert(1)</script>'].
- Setup: Create a post with
- AJAX Handlers: Check for
wp_ajax_handlers that save metadata.- Grep:
grep -r "wp_ajax" .
- Grep:
- Template Overrides: Check if the plugin allows users to define custom "Template" strings or "CSS" blocks in the settings, which are often overlooked for sanitization.
Summary
The Greenhouse Job Board plugin for WordPress is vulnerable to Stored Cross-Site Scripting via its settings page in versions up to 2.7.3. On installations where unfiltered_html is restricted (such as Multi-site), an authenticated administrator can inject malicious JavaScript into settings fields that lack proper input sanitization and output escaping, which executes when the settings page is viewed.
Vulnerable Code
// Inferred from research plan code flow // Settings are registered without a sanitization callback register_setting('ghjb_settings_group', 'ghjb_id'); --- // Stored options are echoed directly into HTML attributes without escaping // Likely located in an admin settings template file <input type="text" name="ghjb_id" value="<?php echo get_option('ghjb_id'); ?>" />
Security Fix
@@ -10,1 +10,1 @@ - register_setting('ghjb_settings_group', 'ghjb_id'); + register_setting('ghjb_settings_group', 'ghjb_id', 'sanitize_text_field'); @@ -25,1 +25,1 @@ - <input type="text" name="ghjb_id" value="<?php echo get_option('ghjb_id'); ?>" /> + <input type="text" name="ghjb_id" value="<?php echo esc_attr(get_option('ghjb_id')); ?>" />
Exploit Outline
To exploit this vulnerability, an attacker requires Administrator-level authentication on a site where unfiltered_html is disabled (e.g., WP Multi-site). The attacker first navigates to the Greenhouse Job Board settings page to extract a valid security nonce (_wpnonce) and the 'option_page' identifier. Using these, they send a POST request to /wp-admin/options.php containing a payload like '"><script>alert(document.domain)</script>' assigned to a plugin setting parameter (e.g., ghjb_id). The payload is stored in the database and executes in the context of any user who subsequently visits the settings page.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.