WordPress shortcode-execution primitives, hidden-field upload bypass, encrypted-blob dispatch, and header-derived bot identity (Sept 19 03:32Z wave)¶
Source: hourly offensive-security scan of GitHub Security Advisories, 2026-09-19 (wave published 2026-09-19T03:32Z).
One theme ties the durable records: a protective transformation is applied to the wrong side of the trust boundary. The PDFCrowd plugin AES-encrypts attacker-influenced option state and later dispatches a callable field out of it; WP Recipe Maker strips shortcodes after do_shortcode() has already executed them; Gravity Forms validates visible fields but persists rejected files through a hidden-field code path; Better Messages identifies its privileged internal bot by prefix-matching a field the client fills in via X-Real-IP.
Advisory table¶
| Product | GHSA | CVE | CVSS | Boundary |
|---|---|---|---|---|
| Save as PDF by PDFCrowd ≤ 4.6.1 | GHSA-3fg9-62hh-37mh | CVE-2026-92807 | 8.8 | eval_shortcode() copies any non-button_/non-email_ shortcode attribute verbatim into the options array; create_button() AES-encrypts the attacker-chosen pdf_created_callback value and embeds the blob in rendered HTML. The unauthenticated wp_ajax_nopriv_save_as_pdf_pdfcrowd endpoint decrypts and invokes $options['pdf_created_callback'] as a PHP callable (line 1722) with plugin option data as sole argument — no is_callable() allowlist, no capability check. Contributor-level shortcode placement yields a cryptographically valid blob (the server encrypts it for you) usable by any anonymous visitor → invoke arbitrary functions/static methods → disclosure of the stored PDFCrowd API key and username |
| Gravity Forms ≤ 3.1.0.4 | GHSA-x9x4-5g6j-pvj3 | CVE-2026-84434 | 9.8 | Mismatch between the field-validation pipeline and the file-persistence pipeline: file upload fields with Visibility = Hidden bypass extension validation, and a rejected file's intact upload state is later passed to upload_file() without re-validation → unauthenticated arbitrary file upload (RCE possible) on any public form containing a hidden File Upload field |
| WP Recipe Maker ≤ 10.8.1 | GHSA-x57m-9xmj-gh9p | CVE-2026-89274 | 9.1 | WPRM_Metadata::sanitize_metadata() recursively runs do_shortcode() on every scalar metadata field — including reviewBody, populated verbatim from approved comment content — before wp_strip_all_tags()/strip_shortcodes(), which operate only on post-execution output. Unauthenticated attacker (comment-approval gate only) executes arbitrary registered site shortcodes server-side on every recipe render, with the output embedded in the page's JSON-LD reviewBody — a built-in disclosure channel (attachment captions, private post fields, whatever installed shortcodes expose) |
| Forminator ≤ 1.57.2 | GHSA-2vrv-wr6r-v79v | CVE-2026-92229 | 9.1 | Front-end action handler passes an unvalidated value into do_shortcode() → unauthenticated arbitrary shortcode execution (quiz front-action path, front-action.php L837/L870) |
| Better Messages ≤ 2.15.33 | GHSA-53vp-fjc5-cqj6 | CVE-2026-89093 | 5.3 | is_ai_bot_user() identifies privileged internal AI-bot accounts by prefix-checking ai-chat-bot- against the guest record's stored IP, which is populated verbatim from the client-controlled X-Real-IP header during unauthenticated guest registration. Register a guest with X-Real-IP: ai-chat-bot-0.0.0.1 → the plugin treats you as its own bot: the check short-circuits user_can_join() and user_can_read() (role allowlist, draft-status, join filters all skipped) → join administrator-restricted chat rooms, post into them, read other users' private history |
Why this is worth an operator page¶
- Server-side encryption of attacker-influenced state is not sanitization. The PDFCrowd blob is only trusted because the server made it: a Contributor places a shortcode with
pdf_created_callback=<function>, the plugin encrypts that into the public page, and the anonymous AJAX endpoint decrypts-and-dispatches. Audit rule: find every decrypt/read-then-act endpoint and check whether any field inside the envelope reaches a callable, path, URL, or SQL sink — integrity protection says nothing about field semantics. Same shape as the Sept 18 React Router unsigned-cookie item's mirror image: signed/encrypted ≠ sanitized. - Field visibility settings are validator-skip switches. Hidden Gravity Forms fields never reach the extension check, yet the persistence pipeline still consumes their upload state. On any form builder or multi-stage pipeline (validate stage ≠ persist stage), enumerate per-field configuration toggles (hidden, disabled, conditional-logic) and test whether the persist leg re-validates. Send an inert PHP-marker file with a dangerous extension through a hidden upload field on a lab form and record whether
upload_file()is reached. do_shortcode()is an execution primitive and the shortcode registry is an internal function namespace. Both recipes (WP Recipe Maker, Forminator) execute untrusted text as shortcodes; WP Recipe Maker adds a rendered-output disclosure channel (JSON-LD) that makes the primitive a data-read, not just a nuisance. Bug-hunt sweep: grep plugin code fordo_shortcode(where the argument can originate from comment content,$_POST, or option/user meta; then enumerate the site's registered shortcodes (rest_shortage-style discovery or logged-out page fuzz for[shortcode]residue) and probe each one for an unauthenticated read/write side effect. Sanitize-vs-execute ordering ("strip after run") is the tell.- Privileged internal identities must never be derived from client-controlled fields. Better Messages' bot check is the request-derived-identity axis (see the Sept 17 kcp
X-Remote-*and TinaclientIDpages) applied to an application-internal service account: an identity discriminator read fromX-Real-IP,X-Forwarded-For, user-agent, or guest-name fields is spoofable. Sweep: find feature names of the form "AI bot", "system", "internal" in JS bundles/AJAX actions and ask what marks a caller as that principal.
Validation workflow (authorized scope only)¶
Lab installs and inert proofs only
Use disposable WordPress installs with the vulnerable plugin versions, synthetic users, fake API keys, and inert marker files. Never invoke production callables, read real wp_users rows, upload executable code to a target, or post into real user chat rooms.
- Encrypted-blob dispatch (PDFCrowd class): in a lab install, place the plugin's shortcode as a lab Contributor with
pdf_created_callbackset to a no-op function that writes a marker file or returns a benign value; POST the page-embedded blob to the nopriv AJAX endpoint as an anonymous curl session. Evidence: marker hit / options-array reflection in the response, plus the fixed-version behavior (allowlist or removal of the callable path). Redact the plugin API key if it reflects. - Hidden-field upload matrix (Gravity Forms): build a lab form with a File Upload field, toggle Visibility visible→hidden. Submit an inert marker file (
.phpname, GIF content) on both variants; record the validation verdict vs whether the file lands inwp-content/uploads/gravity_forms/. Evidence is the upload-state decision table + file listing; do not execute the uploaded marker. - Shortcode-execution probe (WP Recipe Maker / Forminator class): on a lab site, register a trivial lab shortcode that echoes a static canary token (no data access); inject
[lab-canary]into a rated comment or Forminator action param and diff rendered page/JSON-LD bytes for the canary. Positive proof = canary text appears in rendered output sourced from the metadata, not the raw stored comment. Enumerate third-party shortcode read sinks only against installed lab plugins. - Header-derived identity spoof (Better Messages class): two lab users; from an anonymous session register a guest with
X-Real-IP: ai-chat-bot-lab, then attempt join/read on a room whoseonly_joined_can_readis flipped to1(so the default-open axis is excluded). Evidence: join verdict + presence-only message visibility in the decision table using synthetic lab messages only. - Pipeline-revalidation greps: for any plugin with a validate→persist split, read the persist function and list which validations it repeats. A persist leg that consumes stored upload/validation state without re-checking is the Gravity Forms class, regardless of the visibility trigger.
Processed without publication¶
Folds: Divi Essential conditional-nonce full-database read (CVE-2026-15760, GHSA-hcxp-7h6p-263r) folded onto the July 28 WordPress page — the nonce check runs only when the nonce POST parameter is present (exact twin of the parameter-ABSENT Filter Gallery/Clean Login axis) and no current_user_can() exists, giving subscribers full table enumeration + caller-controlled row reads of wp_users/wp_usermeta/wp_options. WP Photo Album Plus escapeshellcmd()-over-whole-command argument injection into an ImageMagick exec() (CVE-2026-87909, GHSA-mx4x-6r5x-jrh9) folded onto the Aug 5 command-wrapper section — DB-layer filename sanitization never applied to the physical temp path; escapeshellcmd leaves spaces as argument separators.
Tracked, axes already covered or detail-light: Tutor LMS student_id IDOR email/phone enumeration (CVE-2026-89333) + subscriber arbitrary-post delete via reparent-to-owned-attachment chain (CVE-2026-88944, interesting ownership-manufacturing step, same parent-reparent axis as prior WordPress pages) + reflected XSS (CVE-2026-89081); Better Messages transcript read at default only_joined_can_read=0 (CVE-2026-89334); SEO Booster admin_init OAuth-callback missing capability → attacker-supplied token drives a stored outbound Google API response (CVE-2026-15660); Pochipp sanitize_text_field-into-attribute XSS (CVE-2026-92967); WP2Social/Asset CleanUp/WPComplete stored-XSS singles (CVE-2026-12042/13354/77820); Divi disclosure folded above; Totolink A3002MU router singles (CVE-2026-93738/93739/93740), Vinyl Cache buffer overflow (CVE-2026-93894), Microsoft Edge UAF (CVE-2026-88097) — product-specific memory-safety/router items with no new operator axis.
Reporting checklist¶
- exact plugin slug, version, form/field configuration state (hidden vs visible,
only_joined_can_read, comment auto-approval), route/action, and auth context (anonymous vs Contributor); - for blob dispatch: encrypt-site, decrypt-site, callable-field name, invoking session, and the fixed-version control (allowlist/removed);
- for upload pipelines: field config, validation verdict per leg, persisted path, and file-content proof (marker bytes, never execution);
- for shortcode execution: source field (comment content / action param), approval-gate state, executing render context, and the output channel where canary text surfaced (JSON-LD field, page region);
- for header spoof: header name, stored field written, principal matched, and the short-circuited authorization functions named;
- bounded impact language: shortcode execution ≠ code execution unless a registered shortcode with a dangerous sink is demonstrated; encrypted-blob callable invocation ≠ API-key disclosure until the key actually reflects in lab output.
September 21 tracked siblings¶
- Canva HarmonyOS app (GHSA-mxw2-g58m-hfwm / CVE-2026-90860, 7.1): a privileged WebView returned sensitive response headers to an external origin loaded inside it → session theft by whoever controls that WebView content. Axis worth remembering for mobile assessments: enumerate which WebView contexts receive auth-bearing headers, and test whether an embedded third-party origin can read them; same "privileged context hands credentials to untrusted frame" family as the header-derived-principal items above. App-store-mobile precondition; tracked, not promoted standalone.
- Drogon ORM
makeCriteriafilter SQLi (GHSA-c6jr-7xqv-9vjv / CVE-2026-94144) — filter-position SQLi in a C++ ORM; order-by/filter-key class already covered by the Fleet/ClickHouse key-position lessons. Sparse VulDB detail; tracked. - ScadaBR export-endpoint disclosure is promoted on the Sept 19 alternate-surface page (September 21 follow-up section) as the import-gated/export-ungated mirror-leg exemplar.
- xxl-job task-name/author reflected XSS, Omega Solution HRM/FBP singles, BioStar BSMEM64 write-what-where IOCTL (fourth of the driver-IOCTL class), Netty CompressionFilter decompression-bomb DoS (CVE-2026-47321 — DoS-only per parser precedent) — tracked, no new axis.
October 8 09:31Z follow-up: Bricksforge ≤3.1.8.9 — a public nonce-mint endpoint plus validate-path/write-name divergence = unauth PHP upload (CVE-2026-85097 / GHSA-jfvp-gc8p-45gp, critical)¶
Unauthenticated RCE chain on one of the larger WordPress builder plugins, and it is a two-key chain where each key is its own durable audit axis:
- Nonce-minting AJAX endpoints collapse the nonce gate. The plugin exposes
bricksforge_regenerate_nonceas an ordinarywp_ajax_(nopriv?)action that hands any anonymous caller a valid nonce. Every "nonce required" protection on the plugin's other handlers inherits this hole. Hunt rule: before theorizing around a WP nonce, enumerate AJAX actions for generators (regenerate_nonce,get_nonce,refresh,tokenshapes in the action-name space) — a nonce is an anti-CSRF capability, not an authorization token, and any endpoint that mints it on request makes the nonce a free parameter for anonymous scripts. Grab the nonce, then attack the upload handler in the same session. - Validation target ≠ write target is the upload-boundary escape. The multipart leg enforces MIME/type validation correctly on the uploaded file (GIF content with a polyglot payload passes as GIF). The divergence is in the submitted
temporaryFileUploadsparameter: it carries both a server-side path pointing at the validated GIF and an attacker-controlledurlfield ending.php— the server persists/renames the validated content under the attacker's name, and the temp upload directory executes it. This page's Gravity Forms leg was a validate-leg/persist-leg mismatch by field visibility; this is the same mismatch by parameter pair: one field says what was checked, the other says what it becomes. Sweep rule for any form builder with staged/temp uploads: for every parameter that names a stored file, list every co-submitted field that names a destination path/name/extension and test whether validation is re-applied at the rename/copy step. Proof stays inert: GIF-header marker bytes + a.phpname that only ever reachesfile_get_contentsof aphpinfo()-free marker — never a web shell. - Report framing: chain the two keys explicitly (nonce source action → handler action → parameter fields → final resolved path + execution proof by benign marker echo), and version-pin; ≤3.1.8.9 is the vulnerable band.
October 14 03:30Z follow-up: Abandoned Cart Pro ≤10.8.0 — MAC-free CTR with attacker-selected key + optional HMAC = any-cart ATO, and Click to Chat for WhatsApp ≤4.44 — sanitize-before-substitute ordering (Oct 14 WP wave)¶
Two legs land directly on this page's "protective transformation applied to the wrong side of the boundary" invariant, and the first one is the strongest crypto-misuse ATO this product family has published.
- Abandoned Cart Pro for WooCommerce ≤10.8.0 authentication bypass via malleable encryption (CVE-2026-97657 / GHSA-j8r3-gcq3-h8jj, critical 9.8): cart-recovery tokens are AES-CTR with no MAC; the 8-byte CTR nonce travels inside the ciphertext blob, and the decryption key is looked up by the attacker-supplied
user_emailGET parameter. The HMAC check inwcap_email_track_links()runs only whenwcap_nonceis non-empty — omit it and the check never fires. Result: an attacker abandons one cart with their own email, gets one legitimate recovery link, recovers the keystream from known plaintext, XOR-forge a ciphertext that decrypts to any victim'sabandoned_id(IDOR on the decrypted value, no ownership check), and the recovery flow callswp_set_auth_cookie()for the victim user — full ATO of any user who ever abandoned a cart, admins included. The auto-login feature defaults ON and is force-enabled on plugin upgrade, so every install is exploitable as shipped. Durable axes: (1) unauthenticated-CTR keystream recovery is a first-class hunt target — any token format that embeds its own IV/nonce and decrypts under a key the caller can influence (email, username, tenant slug as key-selector) leaks keystream to anyone with one valid token; grep crypto helpers for key derivation from request parameters and nonce-inside-blob layouts; (2) conditional integrity checks fail by omission — the Oct 8 Enswitch absent-parameter battery applies to crypto gates: build the decision table {HMAC valid, HMAC wrong, HMAC absent via omitted companion param} × {accept, reject}; a check gated onif ($nonce !== '')is a check that requests to be skipped; (3) decrypted value still needs an authorization check — the page's dispatch-decrypted-blob rule generalizes: decrypt ≠ authenticate, and here the plaintext's subject ID is trusted with zero binding to the key-selected identity. Validation boundary: two disposable accounts on a lab install, forge against your own second account's cart only, never real shops. - Same product, same wave — second-order SQLi through stored cart meta (CVE-2026-97668 / GHSA-xc7m-7p9h-4hfm 9.8; CVE-2026-102251 / GHSA-fwwh-fmqj-qv4w 7.5): the guest cart's
abandoned_cart_info— populated from checkout fields like the SMS-consent input — passes throughstripslashes()and is concatenated into anUPDATE wp_usermetawhile the adjacent SELECT/INSERT use prepared statements; thevalidateparameter leg is the same shape wrapped in the forgeable CTR token from the leg above. The UPDATE-on-usermeta sink writes awp_capabilitiesrow = admin elevation from an unauthenticated checkout field. Axes: (1) prepared-statement sibling parity — when one statement in a function is parameterized and its neighbor concatenates, the neighbor is the bug; read every SQL statement in a handler, not just the one you fuzzed; (2)stripslashes()is not escaping, and stored-field sinks get a second chance — the payload rides checkout fields → cart meta → later UPDATE; trace stored untrusted values forward to every query that consumes them (this page's deferred-detonation cousin); (3) crypto-malleable tokens convert any downstream SQLi into a stealth channel — the token validates structurally while the SQL rides inside. - Click to Chat for WhatsApp ≤4.44 arbitrary shortcode execution (CVE-2026-92097 / GHSA-4768-hg42-39c5, critical 9.1):
greetings_dialog()builds the{url}template placeholder fromsanitize_text_field($_SERVER['REQUEST_URI'])on archive pages and substitutes it into greeting content that then goes todo_shortcode()— butwp_kses()runs againstmain_contentBEFORE the{url}substitution, so attacker-controlled URL bytes skip the sanitizer entirely and reach the shortcode executor unfiltered (unauthenticated). This is the WP Recipe Maker ordering rule with a new mechanism: the sanitizer's target and the substituted content are different strings at different times — sanitizing the template before placeholder fill sanitizes everything except the payload. Sweep rule: for every template/placeholder pipeline ({url},{name}, mail-merge style), ask whether filtering happens pre-substitution (broken, attacker bytes survive into the sink) or post-substitution (correct); then test by putting shortcode delimiters[]in the URL path, not the field. Same page's invariant, third mechanism after strip-after-run and callable-in-blob.