xss_stored
DynamicData::render_block_data runs on every block render (frontend and REST-rendered content) with no capability check. It regex-replaces {{{ field }}} (optional whitespace) in innerHTML and each innerContent string by calling ACF get_field($field, get_the_ID()) and inserting the return value unchanged. get_field() is invoked with two arguments, so ACF 6.2.6+ $escape_html stays false and the value is raw post meta. WordPress render_block() then echoes those chunks as the HTML document; <script> and inline event handlers in the field execute for every visitor of the published post. Write path is the post editor (core edit_post + ACF’s own nonce): Subscriber/customer cannot use it; Contributor can save the payload on their own posts (and add the placeholder as ordinary paragraph text, which kses does not strip); Author can publish. That is an unfiltered_html bypass — WordPress kses post_content for those roles, but ACF text/textarea values are stored as meta and this plugin splices them into the page at view time. The field name is taken from the token ([^}]+), so the actor can also surface ACF fields that the field-group UI hides from their role. Reusable core/block is skipped, but nested innerBlocks are processed recursively. Util::get_field() is a separate unused wrapper and is not this sink. Assets::init is commented out and is not involved.
src/DynamicData.php:23-49
RecommendationEscape at the sink: pass true as get_field()'s fourth argument (ACF ≥ 6.2.6) or wrap the return in wp_kses_post() / esc_html() depending on whether the token is meant to allow HTML. If raw HTML is required, restrict interpolation to content last saved by a user with unfiltered_html, or allowlist field names instead of accepting [^}]+ from post_content. Do not treat Contributor-authored {{{ … }}} as a trusted template. Keep ACF inactive-safe with function_exists('get_field') so missing ACF is not a fatal on every render.