HexBrain

Automattic · Report #3447021

XSS Vulnerability on Pressable/Atomic Hosting Platform via unescaped admin notices leads to code execution

Validstatus
76upvotes
18 Feb 2026disclosed

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:

  1. Update or set the atomic_single_option_limiter_notices option on a site to a:1:{s:34:"<script>alert('test')</script>test";a:1:{s:7:"expires";i:1893456000;}}
  2. Visit /wp-admin/plugins.php
  3. 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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original