Automattic · Report #3447021
XSS Vulnerability on Pressable/Atomic Hosting Platform via unescaped admin notices leads to code execution
How it was found
Summary:
XSS Vulnerability: Unescaped text output in admin notice on atomic-platform.php
Platform(s) Affected:
Pressable / Atomic Hosting Platform
Steps To Reproduce:
- Update or set the
atomic_single_option_limiter_noticesoption on a site toa:1:{s:34:"<script>alert('test')</script>test";a:1:{s:7:"expires";i:1893456000;}} - Visit
/wp-admin/plugins.php - The XSS payload will execute in the admin notice area
This vulnerability is contingent on getting the XSS payload into the options table first. Once injected, it will be executed when other users view the affected admin page.
Supporting Material/References:
Vulnerable code in atomic-platform.php:
function add_admin_notice( $message, $class = 'notice notice-error is-dismissible' ) {
$this->admin_notices[] = array( $message, $class );
}
function print_admin_notices() {
foreach ( $this->admin_notices as $notice ) {
?>
<div class="<?php echo esc_attr( $notice[1] ); ?>">
<p><?php echo $notice[0]; ?></p>
</div>
<?php
}
}
The notices are echoed without any escaping -- missing esc_html() around the $notice[0]. This is likely done because there can be some markup in the notice -- <strong> tags, for example:
function single_option_limiter_notices() {
$admin_notices = get_option( $this->single_option_limiter_notices_key );
if ( empty( $admin_notices ) || ! is_array( $admin_notices ) ) {
return;
}
$changed = false;
foreach ( $admin_notices as $option => $data ) {
if ( $data['expires'] > time() ) {
$this->add_admin_notice(
"Excess updates to option <strong>$option</strong> blocked."
);
} else {
unset( $admin_notices[ $option ] );
$changed = true;
}
}
if ( $changed ) {
update_option( $this->single_option_limiter_notices_key, $admin_notices );
}
}
However, even here, when they are set from being pulled in from the options table, they aren't escaped when interpolated into the string.
Recommended fix: Escape the output in the print_admin_notices() method, or earlier using something like:
$this->add_admin_notice(
sprintf( 'Excess updates to option <strong>%s</strong> blocked.', esc_html( $option ) );
);
Note: The admin notice comes with is-dismissible class, but dismissing it does nothing as it reappears on page refresh.