VYPR
Medium severity5.9GHSA Advisory· Published Aug 22, 2025· Updated Apr 15, 2026

CVE-2025-8678

CVE-2025-8678

Description

The WP Crontrol plugin for WordPress is vulnerable to blind Server-Side Request Forgery in versions 1.17.0 to 1.19.1 via the 'wp_remote_request' function. This makes it possible for authenticated attackers, with Administrator-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
johnbillion/wp-crontrolPackagist
>= 1.17.0, < 1.19.21.19.2

Affected products

1

Patches

1
b085bd306588

Merge commit from fork

https://github.com/johnbillion/wp-crontrolJohn BlackbournAug 19, 2025via ghsa
9 files changed · +123 19
  • phpstan.neon.dist+2 0 modified
    @@ -14,6 +14,8 @@ parameters:
     		- tests/phpstan/stubs.php
     	dynamicConstantNames:
     		- CRONTROL_DISALLOW_PHP_EVENTS
    +	WPCompat:
    +		pluginFile: wp-crontrol.php
     	ignoreErrors:
     		-
     			identifier: requireOnce.fileNotFound
    
  • SECURITY.md+1 1 modified
    @@ -4,4 +4,4 @@
     
     [You can report security bugs through the official WP Crontrol Vulnerability Disclosure Program on Patchstack](https://patchstack.com/database/vdp/wp-crontrol). The Patchstack team helps validate, triage, and handle any security vulnerabilities.
     
    -Do not report security issues on GitHub or the WordPress.org support forums. Thank you.
    +Do not report security issues on GitHub, on the WordPress.org support forums, or via email. Thank you.
    
  • src/bootstrap.php+2 2 modified
    @@ -1765,7 +1765,7 @@ function show_cron_form( $editing ) {
     								);
     							}
     							?>
    -							<input type="url" class="regular-text code" id="crontrol_url" name="crontrol_url" value="<?php echo esc_url( $is_editing_url ? $existing['args'][0]['url'] : '' ); ?>" />
    +							<input type="url" class="regular-text code" id="crontrol_url" name="crontrol_url" value="<?php echo esc_attr( $is_editing_url ? $existing['args'][0]['url'] : '' ); ?>" />
     							<?php do_action( 'crontrol/manage/url', $existing ); ?>
     						</td>
     					</tr>
    @@ -2720,7 +2720,7 @@ function action_url_cron_event( array $args ): void {
     			home_url( '/' )
     		),
     	);
    -	$response = wp_remote_request( $url, $request_args );
    +	$response = wp_safe_remote_request( $url, $request_args );
     
     	if ( is_wp_error( $response ) ) {
     		throw new Exception(
    
  • src/event-list-table.php+25 7 modified
    @@ -354,11 +354,7 @@ protected function extra_tablenav( $which ) {
     	public function single_row( $event ) {
     		$classes = array();
     
    -		if ( ( 'crontrol_cron_job' === $event->hook ) && isset( $event->args[0]['syntax_error_message'] ) ) {
    -			$classes[] = 'crontrol-error';
    -		}
    -
    -		if ( integrity_failed( $event ) ) {
    +		if ( self::row_has_error( $event ) ) {
     			$classes[] = 'crontrol-error';
     		}
     
    @@ -437,6 +433,7 @@ protected function handle_row_actions( $event, $column_name, $primary ) {
     
     		// PHP cron events can be edited as long as they are enabled and the user has permission.
     		$can_edit = ( 'crontrol_cron_job' !== $event->hook ) || ( self::$can_manage_php_crons && self::$php_crons_enabled );
    +		$has_error = self::row_has_error( $event );
     
     		if ( $can_edit ) {
     			$link = array(
    @@ -462,9 +459,9 @@ protected function handle_row_actions( $event, $column_name, $primary ) {
     		}
     
     		// PHP cron events can be run as long as they are enabled.
    -		$can_run = ( 'crontrol_cron_job' !== $event->hook ) || self::$php_crons_enabled;
    +		$can_run = ( ( 'crontrol_cron_job' !== $event->hook ) || self::$php_crons_enabled ) && ! $has_error;
     
    -		if ( ! is_paused( $event ) && ! integrity_failed( $event ) && $can_run ) {
    +		if ( ! is_paused( $event ) && $can_run ) {
     			$link = array(
     				'page'                  => 'wp-crontrol',
     				'crontrol_action'       => 'run-cron',
    @@ -551,6 +548,21 @@ protected function handle_row_actions( $event, $column_name, $primary ) {
     		return $this->row_actions( $links );
     	}
     
    +	/**
    +	 * @param stdClass $event The cron event for the current row.
    +	 */
    +	private static function row_has_error( $event ): bool {
    +		if ( 'crontrol_cron_job' === $event->hook && isset( $event->args[0]['syntax_error_message'] ) ) {
    +			return true;
    +		}
    +
    +		if ( 'crontrol_url_cron_job' === $event->hook && isset( $event->args[0]['url_error_message'] ) ) {
    +			return true;
    +		}
    +
    +		return integrity_failed( $event );
    +	}
    +
     	/**
     	 * Outputs the checkbox cell of a table row.
     	 *
    @@ -656,6 +668,12 @@ protected function column_crontrol_hook( $event ) {
     				$output = esc_html__( 'URL cron event', 'wp-crontrol' );
     			}
     
    +			if ( isset( $event->args[0]['url_error_message'] ) ) {
    +				$output .= '<br><span class="status-crontrol-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span> ';
    +				$output .= esc_html( $event->args[0]['url_error_message'] );
    +				$output .= '</span>';
    +			}
    +
     			return $output;
     		}
     
    
  • src/event.php+58 1 modified
    @@ -144,6 +144,7 @@ function add( $next_run_local, $schedule, $hook, array $args ) {
     	}
     
     	$next_run_utc = (int) get_gmt_from_date( gmdate( 'Y-m-d H:i:s', $next_run_local ), 'U' );
    +	$error = null;
     
     	if ( 'crontrol_cron_job' === $hook && ! empty( $args[0]['code'] ) ) {
     		try {
    @@ -163,6 +164,16 @@ function add( $next_run_local, $schedule, $hook, array $args ) {
     		} catch ( \ParseError $e ) {
     			$args[0]['syntax_error_message'] = $e->getMessage();
     			$args[0]['syntax_error_line'] = $e->getLine();
    +			$error = $e;
    +		}
    +	}
    +
    +	if ( 'crontrol_url_cron_job' === $hook && ! empty( $args[0]['url'] ) ) {
    +		try {
    +			validate_url( $args[0]['url'] );
    +		} catch ( \InvalidArgumentException $e ) {
    +			$args[0]['url_error_message'] = $e->getMessage();
    +			$error = $e;
     		}
     	}
     
    @@ -176,7 +187,14 @@ function add( $next_run_local, $schedule, $hook, array $args ) {
     		return $result;
     	}
     
    -	return true;
    +	return ( $error instanceof \Throwable ) ? new WP_Error(
    +		'has_error',
    +		sprintf(
    +			/* translators: %s: The error message. */
    +			__( 'The cron event was saved but contains an error: %s.', 'wp-crontrol' ),
    +			$error->getMessage(),
    +		),
    +	) : true;
     }
     
     /**
    @@ -557,3 +575,42 @@ function get_core_cron_array() {
     
     	return $crons;
     }
    +
    +/**
    + * Validates a URL for a cron event.
    + *
    + * @see https://github.com/WordPress/wordpress-develop/blob/197f0a71ad27d0688b6380c869aeaf92addd1451/src/wp-includes/class-wp-http.php#L283-L299
    + *
    + * @throws \InvalidArgumentException If the URL is not valid or contains an invalid protocol.
    + *
    + * @param string $url The URL to validate.
    + */
    +function validate_url( string $url ): void {
    +	$valid = wp_http_validate_url( $url );
    +
    +	if ( $valid === false ) {
    +		throw new \InvalidArgumentException(
    +			esc_html(
    +				sprintf(
    +					/* translators: %s: The URL that failed validation. */
    +					__( 'The URL "%s" is not allowed', 'wp-crontrol' ),
    +					$url,
    +				)
    +			)
    +		);
    +	}
    +
    +	$filtered = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) );
    +
    +	if ( $filtered === '' ) {
    +		throw new \InvalidArgumentException(
    +			esc_html(
    +				sprintf(
    +					/* translators: %s: The URL that failed validation. */
    +					__( 'The URL "%s" contains an invalid protocol', 'wp-crontrol' ),
    +					$url,
    +				)
    +			)
    +		);
    +	}
    +}
    
  • tests/acceptance/AddEventCest.php+15 0 modified
    @@ -48,6 +48,21 @@ public function AddingANewURLEvent( AcceptanceTester $I ) {
     		$I->see( 'https://example.org/' );
     	}
     
    +	public function AddingANewURLEventWithDisallowedURLShowsError( AcceptanceTester $I ) {
    +		$I->amOnCronEventListingPage();
    +		$I->click( 'Add New Cron Event', '#wpbody' );
    +		$I->selectOption( 'input[name="crontrol_action"]', 'URL cron event' );
    +		$I->fillField( '#crontrol_url', 'http://localhost:22' );
    +		$I->click( 'Add Event' );
    +		$I->see( 'Cron Events', 'h1' );
    +		$I->seeAdminErrorNotice( 'The cron event was saved but contains an error: The URL "http://localhost:22" is not allowed' );
    +
    +		$row = $I->amWorkingWithAnExistingCronEvent( 'http://localhost:22' );
    +		$I->see( 'Edit', $row );
    +		$I->see( 'Delete', $row );
    +		$I->dontSee( 'Run now', $row );
    +	}
    +
     	public function AddingANewPHPEvent( AcceptanceTester $I ) {
     		$I->amOnCronEventListingPage();
     		$I->click( 'Add New Cron Event', '#wpbody' );
    
  • tests/acceptance/DeleteAllWithHookCest.php+6 6 modified
    @@ -12,9 +12,9 @@ public function _before( AcceptanceTester $I ) {
     	}
     
     	public function DeletingAHook( AcceptanceTester $I ) {
    -		$I->amWorkingWithACronEvent( 'example_hook', '[1]' );
    -		$I->amWorkingWithACronEvent( 'example_hook', '[2]' );
    -		$row = $I->amWorkingWithACronEvent( 'example_hook', '[3]' );
    +		$I->amWorkingWithANewCronEvent( 'example_hook', '[1]' );
    +		$I->amWorkingWithANewCronEvent( 'example_hook', '[2]' );
    +		$row = $I->amWorkingWithANewCronEvent( 'example_hook', '[3]' );
     
     		$I->click( 'Delete all events with this hook (3)', $row );
     		$I->acceptPopup();
    @@ -24,9 +24,9 @@ public function DeletingAHook( AcceptanceTester $I ) {
     	}
     
     	public function DeletingAPersistentWordPressCoreHook( AcceptanceTester $I ) {
    -		$I->amWorkingWithACronEvent( 'wp_scheduled_delete', '[1]' );
    -		$I->amWorkingWithACronEvent( 'wp_scheduled_delete', '[2]' );
    -		$row = $I->amWorkingWithACronEvent( 'wp_scheduled_delete', '[3]' );
    +		$I->amWorkingWithANewCronEvent( 'wp_scheduled_delete', '[1]' );
    +		$I->amWorkingWithANewCronEvent( 'wp_scheduled_delete', '[2]' );
    +		$row = $I->amWorkingWithANewCronEvent( 'wp_scheduled_delete', '[3]' );
     
     		$I->click( 'Delete all events with this hook (4)', $row );
     		$I->acceptPopup();
    
  • tests/acceptance/PauseEventCest.php+1 1 modified
    @@ -12,7 +12,7 @@ public function _before( AcceptanceTester $I ) {
     	}
     
     	public function PausingAnEvent( AcceptanceTester $I ) {
    -		$row = $I->amWorkingWithACronEvent( 'pause_me_soon' );
    +		$row = $I->amWorkingWithANewCronEvent( 'pause_me_soon' );
     
     		$I->click( 'Pause', $row );
     		$I->seeAdminSuccessNotice( 'Paused the pause_me_soon hook.' );
    
  • tests/_support/AcceptanceTester.php+13 1 modified
    @@ -99,7 +99,7 @@ public function amOnCronScheduleListingPage() {
     	 * @param string $args     The event arguments encoded as JSON.
     	 * @return string
     	 */
    -	public function amWorkingWithACronEvent( string $hook_name, string $args = '' ) {
    +	public function amWorkingWithANewCronEvent( string $hook_name, string $args = '' ) {
     		$this->amOnCronEventListingPage();
     		$this->click( 'Add New Cron Event', '#wpbody' );
     		$this->fillField( 'Hook Name', $hook_name );
    @@ -109,4 +109,16 @@ public function amWorkingWithACronEvent( string $hook_name, string $args = '' )
     
     		return Locator::contains( '.crontrol-events tr', $hook_name );
     	}
    +
    +	/**
    +	 * Work with an existing cron event.
    +	 *
    +	 * @param string $hook_name The event hook name.
    +	 * @return string
    +	 */
    +	public function amWorkingWithAnExistingCronEvent( string $hook_name ) {
    +		$this->amOnCronEventListingPage();
    +
    +		return Locator::contains( '.crontrol-events tr', $hook_name );
    +	}
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.