---
title: "WordPress LFI Bugs: Two Plugins, Same Shortcode Trick"
slug: "wordpress-lfi-vulnerabilities-shortcode-template-attribute"
description: "Two new WordPress LFI bugs, HUSKY and WP Travel Engine, let attackers include arbitrary PHP files through a shortcode attribute. Same root cause, different plugins."
date: 2026-09-23
author: "WP Vanguard Team"
category: "Research"
tags: ["wordpress-lfi-vulnerability", "local-file-inclusion", "shortcode-attribute-injection", "husky-woocommerce-products-filter", "wp-travel-engine"]
featured_image: "/images/blog/wordpress-lfi-vulnerabilities-shortcode-template-attribute.jpg"
---

Two unrelated WordPress plugins disclosed local file inclusion bugs within a day of each other this week, and both trace back to the same design mistake: a shortcode attribute gets handed straight to a file-loading function with no check on where it's allowed to point.

```stat
{"value": "33", "label": "high or critical local file inclusion disclosures in 30 plugins in the last 90 days", "note": "WP Vanguard database, July 5 to October 4, 2026. 18 of them need no login."}
```

HUSKY, the WooCommerce Products Filter plugin, shipped the worse of the two. Its version is unauthenticated. WP Travel Engine's needs contributor-level access first. Different severity, same root cause, worth covering together because the pattern is what's actually reusable here, not either individual CVE.

## HUSKY's CVE-2026-92969: No Login Required

HUSKY (WooCommerce Products Filter Professional) versions up to and including 1.4.4 pass a `shortcode` parameter into the `woof_draw_products` AJAX handler, which resolves a `custom_tpl` template attribute and includes whatever file path comes out the other end. Wordfence rates it CVSS 8.1, high severity.

What makes this one unauthenticated in practice, not just in theory: the only gate in front of the AJAX action is a nonce check against `woof_front_nonce`. That nonce isn't a secret. It's emitted into inline JavaScript on every front-end page the plugin runs on, which means any site visitor can read it straight out of the page source and use it to call the vulnerable endpoint. A nonce check that's satisfied by a value the page hands out for free isn't an access control.

The fix ships in 1.4.5. If you're running HUSKY, this is a same-day update, not a scheduled-maintenance one.

## WP Travel Engine's CVE-2026-9231: Contributor and Above

WP Travel Engine, the tour booking plugin, has a similar bug in `wte_get_template`, reached through a `template` shortcode attribute. Versions through 6.8.0 are affected, fixed in 6.8.1. Wordfence scores it CVSS 7.5.

The access bar is higher here: it needs an authenticated account with contributor-level permissions or above. That's still a meaningfully low bar on any site that accepts guest posts, has a membership tier, or runs a multi-author blog, since contributor is usually the first role above subscriber. A compromised contributor account, or a contributor account created through a separate vulnerability, gets a path to arbitrary PHP file inclusion it shouldn't have.

## The Shared Root Cause: Shortcode Attributes as File Paths

Both bugs work the same way at the code level. A shortcode attribute value, something meant to be a short string like a template name or a filter type, gets passed into a function that resolves it to a filesystem path and then includes that file. Neither plugin validated that the resolved path stayed inside its own template directory before calling `include` or `require`.

This is the same class of mistake as classic path traversal, just reached through a shortcode instead of a URL parameter or a form field. Shortcode attributes feel like internal, developer-controlled values because they usually appear inside `[shortcode attr="value"]` markup written by a trusted editor. Both of these plugins expose the same attribute-to-file-path pipeline through an AJAX action or public function, where the value comes from an untrusted request instead of trusted editorial content. The lesson generalizes past these two plugins: any function that turns a shortcode, filter, or template attribute into a file path needs to validate the result stays inside an allowed directory, regardless of how the attribute normally gets set.

We've covered the same mechanics before from a file-deletion angle in our [Formidable Signatures writeup](/blog/formidable-signatures-file-deletion-cve-2026-16230): a parameter that should be a filename accepted a path instead, with no containment check.

## A Pattern, Not a Pair

We went back through our database to see whether these two were unusual. They aren't. Between July 5 and October 4 we logged 33 high or critical local file inclusion disclosures across 30 plugins, out of 1,206 high or critical plugin disclosures in the same window. More than half need no login.

```bars
{"title": "Local file inclusion disclosures by access level", "highlight": "No login", "data": [
 {"label": "No login", "value": 18},
 {"label": "Contributor+", "value": 8},
 {"label": "Subscriber+", "value": 4},
 {"label": "Other roles", "value": 3}
], "note": "High or critical, plugins, July 5 to October 4, 2026."}
```

Ten of those 33 advisory titles name the input that carries the file path. Most are a template, layout or page parameter, the same shape as HUSKY and WP Travel Engine. The table lists the nine plugins involved, since Eventin appears twice:

| Plugin | Parameter | Login needed | CVSS | Fixed in |
|---|---|---|---|---|
| Visual Composer | `vcv-template` | None | 9.8 | 45.16.1 |
| Divi Ajax Filter | `custom_loop_template` | None | 9.8 | 5.1.3 |
| HUSKY | `custom_tpl` | None | 8.1 | 1.4.5 |
| WP Travel Engine | `template` | Contributor | 7.5 | 6.8.1 |
| SiteOrigin Widgets Bundle | `theme` | Contributor | 7.5 | 1.74.0 |
| Events Made Easy | `wp_page_template` | Contributor | 7.5 | 3.2.6 |
| Eventin | `event_layout` | Contributor | 7.5 | 4.1.23 |
| WP Maps | `page` | Subscriber | 7.5 | 5.0.0 |
| Simply Schedule Appointments | `ssa_locale` | Subscriber | 7.5 | 1.6.12.33 |

> [!KEY]
> Any plugin that turns a request value into a file path is one missing check away from this bug, and nonces, role checks and shortcode context don't make the value safe.

The two top rows score 9.8. That score isn't unusual for an unauthenticated include. It reflects how little an attacker needs to do, not what the file ends up doing, and that part depends on the site.

## What an Include Bug Can Lead To

Local file inclusion sounds like a read bug, but `include` runs PHP. If the file an attacker points at is a PHP file that does something when loaded, that something happens, with the site's full permissions. Whether it becomes remote code execution depends on what's reachable: a file the attacker can upload, a log they can write into, or a script in another plugin that does something dangerous on its own.

None of the 33 advisories we checked use the words remote code execution in their titles, which is a fair framing, because the outcome is conditional. It's still the same bug class as the WordPress core flaw behind [CVE-2026-87902](/blog/wordpress-7-1-2-rce-cve-2026-87902), where a template path could be steered to a file outside the theme and attackers were exploiting it within hours. Treat an unauthenticated include as serious even when the advisory stops at "high".

## What Plugin Authors Should Change

The fix is the same in every plugin on the list, and it's small. Don't let a request choose a file path at all. Map the value to a fixed list of templates you ship, and fall back to a default when it isn't on the list.

```php
$templates = array(
	'grid' => 'templates/grid.php',
	'list' => 'templates/list.php',
);

$key  = isset( $atts['template'] ) ? sanitize_key( $atts['template'] ) : 'grid';
$file = $templates[ $key ] ?? $templates['grid'];

include plugin_dir_path( __FILE__ ) . $file;
```

If templates really have to be discovered on disk, resolve the final path with `realpath()` and refuse it unless it starts with the plugin's own template directory. Strip directory separators and dots before that check, because a value like `../../wp-config` is the whole attack.

Two habits prevent most of these. Treat shortcode attributes as untrusted whenever the same code can be reached from an AJAX action or a REST route, since that's where HUSKY went wrong. And don't count a nonce as access control when the page hands it to every visitor.

## How to Check and Fix

Confirm your installed versions:

```bash
wp plugin list --name=woocommerce-products-filter --field=version
wp plugin list --name=wp-travel-engine --field=version
```

HUSKY below 1.4.5 or WP Travel Engine below 6.8.1 means you're exposed. Update immediately:

```bash
wp plugin update woocommerce-products-filter
wp plugin update wp-travel-engine
```

If either plugin can't be updated right away, deactivating it removes the exposure entirely since both bugs require the plugin's own AJAX action or shortcode handler to be reachable. For a broader check across everything installed, our guide on [checking WordPress plugins for known vulnerabilities](/blog/check-wordpress-plugins-for-known-vulnerabilities) walks through the WP-CLI routine we run internally, and [WP Vanguard's free surface scan](/#scanner) checks your live site against both CVEs and the rest of our database in under a minute.

### Looking for signs of probing

If you ran a vulnerable version on a public site, your access logs can tell you whether anyone tried it. Search for requests to `admin-ajax.php` or the plugin's pages where a parameter named like the ones in the table contains `../`, `..%2f` or `%2e%2e`. A value that looks like a path, not a template name, is a probe.

A probe in the log doesn't mean it worked, and a clean log doesn't prove it didn't, since many hosts keep only a few days. Export what you have before it rolls off. If you find probes, check the uploads folder for PHP files and list your administrators, using our guide to the [signs of a hacked site](/blog/signs-wordpress-site-hacked).

## References

- [HUSKY (WooCommerce Products Filter) on WordPress.org](https://wordpress.org/plugins/woocommerce-products-filter/#developers)
- [WP Travel Engine on WordPress.org](https://wordpress.org/plugins/wp-travel-engine/#developers)
- [Wordfence Threat Intelligence: CVE-2026-92969](https://www.wordfence.com/threat-intel/vulnerabilities/id/1f71b558-e8c0-441e-b437-a7f30ad13513-woocommerce-products-filter)
- [Wordfence Threat Intelligence: CVE-2026-9231](https://www.wordfence.com/threat-intel/vulnerabilities/id/708b70c7-e4e6-4422-9682-c7d05063a188-wp-travel-engine)
