CVE-2023-2795

the admin XSS that does not care about unfiltered_html: stored XSS in CodeColorer

CodeColorer (<= 0.10.0) echoes its Custom CSS Classes setting straight into a value attribute on the options page with no escaping. Save a payload once and it fires every time that page loads, including on multisite where admins are not allowed to post raw HTML. Found with Ilyase Dehy.

why bother with an admin-only bug

The reflex with admin-only XSS is to wave it off. An administrator can already drop raw HTML into a post, so script in an admin field looks like a non-issue. That reflex is wrong the moment unfiltered_html is off, which is the default on WordPress multisite and anywhere DISALLOW_UNFILTERED_HTML is set. There, even an administrator cannot put a <script> in a post. A plugin setting that stores and reflects script anyway walks straight across that boundary. That gap is the whole finding.

This one was found with Ilyase Dehy while auditing small, popular plugins for unescaped option output.


the sink

CodeColorer has a settings page that renders each saved option back into an input so you can edit it. codecolorer-admin.php:

<input name="codecolorer_css_class" type="text" class="regular-text code" size="60"
       id="codecolorer_css_class"
       value="<?php echo get_option('codecolorer_css_class') ?>"/>

get_option('codecolorer_css_class') is the Custom CSS Classes value, and it goes into the value attribute with a bare echo. No esc_attr, no esc_html, nothing. The value is whatever you saved.


breaking out of the attribute

The value sits inside a double-quoted attribute, so the payload only needs to close the quote and the tag, then open its own:

"><script>alert(1)</script>

Put that in the Custom CSS Classes field and save. The option is written to the database, so this is stored, not reflected. From then on every load of the CodeColorer settings page renders:

<input ... value=""><script>alert(1)</script>"/>

The attribute closes at ">, the input tag ends, and the <script> runs. Any admin who opens the plugin settings executes it, and it keeps firing until the row is cleaned out.


the part that makes it a CVE

On a single-site install with unfiltered_html, an admin already has HTML, so the severity floor is low. The reason it earned an ID is the multisite case: with unfiltered_html revoked, the post and widget paths strip script, but this option output does not. So a super-admin-restricted network where site admins are deliberately denied raw HTML still hands them stored script execution through a plugin setting. CVSS 3.5, low, but it is a real escalation across the unfiltered_html boundary rather than a cosmetic one.


reproduction

Local WordPress, CodeColorer pinned to 0.10.0, admin session.

Settings โ†’ CodeColorer โ†’ Custom CSS Classes

Value:

"><script>alert(1)</script>

Save Changes. The page reloads, the options form re-renders the stored value into the value attribute, the breakout closes the input, and the script fires on load.

CVE-2023-2795 stored XSS firing on the CodeColorer settings page


the fix

0.10.1 wraps the option output in esc_html(), the same change applied to every field on the page:

value="<?php echo esc_html(get_option('codecolorer_css_class')) ?>"

esc_attr() is the textbook choice for an attribute, but esc_html() encodes the ", <, and > that the breakout depends on, so it closes the hole. The wider lesson is the boring one: every get_option() that reaches HTML is output, and output gets escaped at the point of use, attribute or not.


references