How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/wpultimatesecurity/wordpress-security-skills --skill shortcode-block-securityBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Shortcode & dynamic block security
When to use this skill
Use this skill whenever code renders content from shortcodes or dynamic blocks:
- Registering a shortcode with
add_shortcode(). - Reading or outputting shortcode attributes (
$atts) or enclosed content ($content). - Registering a dynamic block with a
render_callback. - Reading block attributes in a server-side render.
- Building HTML, URLs, or classes from shortcode/block input.
Shortcodes and dynamic blocks are stored XSS sinks: a contributor enters [my_card title="<script>..."] and the render callback echoes it unescaped. Every attribute must be sanitized and every output escaped.
Related: see the output-escaping skill for context-correct escaping and the gutenberg-block-editor-security skill for broader block-editor surfaces.
Core principles (and why they matter)
- `shortcode_atts()` sets defaults; it does NOT sanitize. The returned array still holds
raw user input. Sanitize each value before use.
- Block attributes are user input too. Declaring
type: 'string'inblock.jsondoes not
escape HTML or JavaScript for you; sanitize on render.
- Escape at render for the exact context. HTML body →
esc_html(). HTML attribute →
esc_attr(). URL → esc_url(). Rich HTML → wp_kses_post() with an allowlist.
- Do not store unescaped attribute values. If you persist them, sanitize on save and escape
on read.
- Never pass shortcode/block input to `do_shortcode()` or `eval()` uncontrolled. Both can
execute arbitrary shortcodes or code.
- Return, don't echo. Shortcode and block render callbacks must return strings; echoing
produces unexpected output placement.
Step-by-step implementation
- In the shortcode callback, call
shortcode_atts()with a complete default map. - Sanitize each attribute to its expected type (
absint,sanitize_text_field,esc_url_raw,
sanitize_key).
- In a block
render_callback, read attributes from the$attributesarray and sanitize them
the same way.
- Build the markup by concatenating escaped values.
- Return the complete markup string.
- For rich content, use
wp_kses_post()or a tightly scopedwp_kses()allowlist.
Supporting references
| Reference | Load when |
|---|---|
| Shortcode & dynamic block security checklist | Before final verification of the shortcode & dynamic block security controls. |
| Secure shortcode and dynamic block | Implementing shortcode and dynamic-block render callbacks with validated attributes and escaped markup. |
Common AI mistakes / anti-patterns
Mistake 1 — Echoing $atts directly
// ❌ Insecure: stored XSS through the title attribute.
function my_plugin_card_shortcode( $atts ) {
return '<div class="card"><h3>' . $atts['title'] . '</h3></div>';
}// ✅ Secure: default, then sanitize, then escape.
function my_plugin_card_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'title' => '',
'link' => '',
),
$atts,
'my_plugin_card'
);
$title = sanitize_text_field( $atts['title'] );
$link = esc_url_raw( $atts['link'] );
$output = '<div class="card">';
if ( $link ) {
$output .= '<h3><a href="' . esc_url( $link ) . '">' . esc_html( $title ) . '</a></h3>';
} else {
$output .= '<h3>' . esc_html( $title ) . '</h3>';
}
$output .= '</div>';
return $output;
}Mistake 2 — Trusting shortcode_atts() to sanitize
// ❌ Insecure: shortcode_atts only supplies defaults and filters unknown keys.
$atts = shortcode_atts( array( 'class' => '' ), $atts );
echo '<div class="' . $atts['class'] . '">...</div>';// ✅ Secure: sanitize the value after normalizing it.
$atts = shortcode_atts( array( 'class' => '' ), $atts, 'my_plugin_box' );
$class = sanitize_html_class( $atts['class'] );
echo '<div class="' . esc_attr( $class ) . '">...</div>';Mistake 3 — Dynamic block render callback echoing attributes
// ❌ Insecure: block attributes echoed raw.
function my_plugin_render_banner( $attributes ) {
?>
<div class="banner" style="background: <?php echo $attributes['bgColor']; ?>">
<?php echo $attributes['heading']; ?>
</div>
<?php
}// ✅ Secure: sanitize attributes and escape for the output context.
function my_plugin_render_banner( $attributes ) {
$bg_color = isset( $attributes['bgColor'] ) ? sanitize_hex_color( $attributes['bgColor'] ) : '#ffffff';
$heading = isset( $attributes['heading'] ) ? sanitize_text_field( $attributes['heading'] ) : '';
return sprintf(
'<div class="banner" style="background: %s"><h2>%s</h2></div>',
esc_attr( $bg_color ),
esc_html( $heading )
);
}Mistake 4 — Allowing arbitrary HTML through attributes
// ❌ Insecure: an attacker can inject script/event handlers.
function my_plugin_render_note( $attributes ) {
return '<div>' . $attributes['content'] . '</div>';
}// ✅ Secure: constrain rich markup with wp_kses_post.
function my_plugin_render_note( $attributes ) {
$content = isset( $attributes['content'] ) ? $attributes['content'] : '';
return '<div>' . wp_kses_post( $content ) . '</div>';
}Mistake 5 — Running do_shortcode() on untrusted input
// ❌ Insecure: executes arbitrary shortcodes supplied by a visitor.
echo do_shortcode( $_POST['content'] );// ✅ Secure: do not run do_shortcode on user input; if required, sanitize first.
$content = wp_kses_post( wp_unslash( $_POST['content'] ?? '' ) );Correct code examples
A complete secure shortcode and dynamic-block render callback is in `references/secure-shortcode-block.php`.
Checklist
- [ ]
shortcode_atts()provides defaults for every supported attribute. - [ ] Each shortcode attribute is sanitized to its expected type after normalization.
- [ ] Block attributes are sanitized inside
render_callback, not trusted fromblock.jsontypes. - [ ] All rendered output is escaped for its context (
esc_html,esc_attr,esc_url,wp_kses_post). - [ ] CSS classes use
sanitize_html_class()(oresc_attr()with an allowlist). - [ ] Colors use
sanitize_hex_color()where appropriate. - [ ] The callback returns a string; it does not echo directly.
- [ ]
do_shortcode()is never run on untrusted input.




