unsafe_input
POST /wp-json/go-webform/optin is registered with permission_callback __return_true and no nonce. optin() takes $request->get_params() (JSON or form), runs apply_filters('go_webform_options', $params) — Disciple.Tools and Kingdom Training handlers only append list_18/list_22 or list_24/list_22 and overwrite a few contact_fields keys (projects, sources, maybe notes/steps_taken); they never strip extra lists or unknown contact_fields. Pray4Movement does not even inject default lists, so a client-supplied lists array is required and fully attacker-controlled. The handler then looks up the site_link_system row whose dev_key is crm_link, mints an hourly transfer token, and wp_remote_post's email, lists, name, first_name, last_name, contact_fields, source, tags, and named_tags to https://{linked host}/wp-json/crm-email/create with Authorization: Bearer. The public shortcode only sends email/first_name/last_name/source, so extra keys are not a documented form field — they are a privileged CRM write. Observable: subscribe any address to any list ID the CRM accepts (sequential list_18..24 are in this codebase), attach arbitrary tags, and set arbitrary contact_fields (notes, milestones, assigned_to, etc. if the CRM maps them) using this site's site-link identity. No email sanitization, CAPTCHA, or rate limit in 2023.05.11. Upstream master still forwards the same keys (nonce only when home_url is https://disciple.tools).
globals/rest-api.php:23-65
RecommendationTreat the public optin body as an allowlist: accept only email, first_name, last_name, and a site-defined source; ignore client lists/tags/named_tags/contact_fields. Set lists and contact_fields only inside go_webform_options from hardcoded values. sanitize_email() and reject empty/invalid email. Add rate-limiting or a CAPTCHA. Campaigns that need extra fields should authenticate with the site-link token (or a dedicated campaign secret), not share the anonymous route.