Skip to content

False Positive: WordPress shortcode taint flow is missed: callback params are not treated as user input #11826

@invoke1442

Description

@invoke1442

Psalm’s taint engine can follow this flow just fine when the source is explicit:

user input --> - method param / array element --> sprintf(...) --> returned string --> outer echo

I verified that with the playground:

direct param --> sprintf --> echo: https://psalm.dev/r/28a080ab63

That reports TaintedHtml / TaintedTextWithQuotes as expected.

The problem is the WordPress shortcode case. In real code, shortcode attributes arrive via the framework and end up in the callback param $atts, but Psalm does not seem to model that as tainted input.

This leads to a false negative on a real CVE:

  • CVE-2025-6257
  • WordPress plugin: wp_eurofxref
  • stored XSS via shortcode attribute append

Vulnerable pattern:

public function currency_shortcode( $atts ) {
    extract( shortcode_atts( array(
        'append' => ' *',
    ), $atts, 'currency' ) );

    if ( $append ) {
        $output .= sprintf(
            '<span class="eurofxref-append-string">%s</span>',
            $append
        );
    }

    return $output;
}

Fixed version escapes shortcode-controlled values before output:

foreach( array( 'from', 'to', 'between', 'append', 'round_append' ) as $var ) {
    $$var = esc_html( $$var );
}
$to_style = esc_attr( $to_style );

So this looks like a framework-modeling gap, not a general taint-tracking limitation.

What seems to be missing

At least one of these:

  1. WordPress shortcode callback params should be treated as user-controlled input.
  2. Returned HTML from shortcode callbacks should be modeled as flowing into HTML output.
  3. WordPress sanitizers like esc_html() / esc_attr() should be modeled as taint escapes.

Why I think this is a real gap

Psalm already handles the equivalent simplified flow when the source is explicit.

In other words, this is not “Psalm can’t track sprintf” or “Psalm can’t track returned strings”. It can. The missing piece is the WordPress-specific source/sink/sanitizer modeling around shortcode callbacks.

Expected behavior

In taint mode, code like the vulnerable example above should produce an HTML taint issue when shortcode-controlled values are returned as HTML without escaping.

Actual behavior

No issue is reported for the real shortcode case.

Minimal control reproduction

This playground example shows the core taint flow works when the source is explicit:

https://psalm.dev/r/28a080ab63

If WordPress shortcode params were modeled as tainted input, I’d expect the real case to behave similarly.

If built-in WordPress support is out of scope, even documenting the recommended stub/plugin approach for shortcode sources and esc_html / esc_attr would help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions