CVE-2023-2634

the sanitizer that was commented out: stored XSS in Get Your Number

Get Your Number (<= 1.1.3) saves its settings by looping over POST fields. The line that should sanitize each value is still there, just commented out, with the raw assignment sitting right next to it. The values are echoed back into value= attributes with no escaping. Admin+ stored XSS, and the source code tells you exactly when it broke.

reading the diff that never happened

Some bugs you find by tracing data flow. This one you find by reading a single line and noticing the developer left their intentions in the source.

Get Your Number is a small raffle/number plugin. It has a settings page with an admin email, a min and max number, and an event name. I pulled 1.1.3 and opened the save handler, process_gyn_options() in inc/gyn_admin_functions.php.


the comment

// Cycle through all text form fields and store their values in the options array
foreach ( $options as $key => $value ) {
    if ( isset( $_POST[$key] ) ) {
        $options[$key] = $_POST[$key]; //sanitize_text_field( $_POST[$key] );
    }
}

Read line three slowly. The value is taken straight from $_POST[$key]. The call that would have cleaned it, sanitize_text_field( $_POST[$key] ), is commented out and parked at the end of the line like a note to self.

Somebody knew. At some point the code was $options[$key] = sanitize_text_field( $_POST[$key] ); and it got switched off, maybe to debug something, and never switched back. The fix and the bug are on the same line.

There is a nonce check above it (check_admin_referer( 'gyn-settings' )) and a capability check (current_user_can( 'manage_options' )), so this is not a CSRF and not a privilege bug. It is a storage bug. Whatever an admin types is stored verbatim.


the sink

Stored verbatim is only half a vulnerability. The other half is the output. The same fields are printed back on the settings page:

<td><input type="text" name="gyn_event_name"
    value="<?php echo $gyn_options['gyn_event_name']; ?>"/> ...</td>

Raw echo into a double-quoted value=. No esc_attr. So the event name I save comes back inside an attribute I can break out of.

That is the full loop: raw in at line 66, raw out at line 161. The event name field is the cleanest one to abuse because the others feed range( $min, $max ) and want to stay numeric.


the payload

Same constraint as the other unsanitized WordPress plugins I have looked at this week. WordPress slashes $_POST and the plugin never calls wp_unslash(), so a literal quote is stored as \". A backslash means nothing inside an HTML attribute, so the breakout still works, but any quote inside my JavaScript gets wrecked. The payload has to be quote-free.

Break out of the value="..." attribute, drop in an image whose error handler runs without needing a single quote:

"><img src=x onerror=alert(document.domain)>

I keep gyn_min_nr and gyn_max_nr set to small integers so range() does not choke, then put the payload in gyn_event_name.


reproduction

Local WordPress 6.2, plugin pinned to 1.1.3, admin session.

Settings โ†’ Get Your Number โ†’ Event name

Event name:

"><img src=x onerror=alert(document.domain)>

Min 1, max 5, save. The redirect lands back on the settings form, the event name is echoed raw into the value attribute, the breakout closes it, the broken image fires.

[DIALOG] type=alert message="localhost"

CVE-2023-2634 stored XSS firing on the Get Your Number settings page

You can see the wreckage of the breakout in the form itself: the event name field holds the leftover \ and the rest of my markup leaked into the page next to the field label. The red bar is the injected banner. The alert confirms execution.


impact

CVSS 4.4, authenticated. Same story as the other admin+ stored XSS bugs: the real risk is on multisite and on sites that hand plugin settings to roles below super admin. A poisoned event name fires for whoever opens the settings page next.


the fix

Uncomment the line. That is genuinely it:

$options[$key] = sanitize_text_field( $_POST[$key] );

And escape on output for defence in depth:

value="<?php echo esc_attr( $gyn_options['gyn_event_name'] ); ?>"

The plugin trunk still carried 1.1.3 long after the report, so treat any install of this one as live.


references