Segurium Research Starter Templates

Starter Templates (astra-sites) 4.7.7 closes an SSRF in image downloads

Segurium Research 7 min read

Plugin
Starter Templates astra-sites
Affected versions
4.7.6 and earlier
Fixed in
4.7.7
Class
Server-side request forgery
Severity
medium Segurium Research assessment. No CVSS score published yet.
CVE
None assigned at the time of writing
Installs
1,000,000 active
Patch released
Sources
Advisory record for Starter Templates 4.7.7 The same finding as a structured entry in the advisory directory.

Starter Templates is a Brainstorm Force plugin with the wordpress.org slug astra-sites. The wordpress.org directory lists 1,000,000+ active installs for the plugin. Version 4.7.7 carries the fix. The vendor’s changelog dates 4.7.7 to 16 September 2026, eight days after 4.7.6.

The changelog names two security changes for 4.7.7. The first reads: “Image sideload endpoints now download from approved stock image hosts only”. The second restricts four endpoints “to administrators only”. The changelog lists them as “the CartFlows, Cart Abandonment Recovery and LatePoint import endpoints and the template data request endpoint”.

The diff between 4.7.6 and 4.7.7 backs both changes. Two image download handlers gain a host allowlist. The allowlist closes a server-side request forgery (SSRF) bug, where the server fetched any URL the caller chose. Four AJAX handlers raise their capability check from edit_posts to manage_options. The only other code change is astra_sites_localize_script_multiline(), a fix for a 404 page on some Apache and LiteSpeed hosts.

What the release fixes

In 4.7.6, two image sideload handlers took a URL from the request and downloaded that file on the server. Neither handler checked the host. So a caller could point the WordPress server at machines on its own private network. Four more AJAX handlers let any account with edit_posts through, and Contributors hold that capability.

  • Affected versions: 4.7.6 and earlier
  • Fixed in: 4.7.7
  • Class: server-side request forgery (SSRF), plus a weak capability check on four AJAX handlers
  • Installs: 1,000,000+ active installs, per the Starter Templates directory page

How the bug works

Starter Templates bundles a library called ZipWP Images under inc/lib/zipwp-images/. Release 4.7.7 moves the library from 1.0.31 to 1.0.32. The class Zipwp_Images_Api holds a handler that downloads an image and saves the file. The diff does not show the handler’s name or the hook that registers the handler.

The handler keeps the requested URL in $url. In 4.7.6, the only test on $url inside the diff is a check for an empty value. Release 4.7.7 adds a host check straight after that test:

 			wp_send_json_error( __( 'Need to send URL of the image to be downloaded', 'astra-sites' ) );
 		}
 
+		// Reject non-allowlisted hosts (SSRF guard).
+		if ( ! $this->is_allowed_image_url( $url ) ) {
+			wp_send_json_error( __( 'Invalid image URL.', 'astra-sites' ) );
+		}
+
 		$image  = '';
 		$result = array();

The code between this hunk and the next one is outside the diff. So the diff does not show the call that fetches the URL. Further down, the class has a method whose docblock reads “Create the image and return the new media upload id.”

The guard calls is_allowed_image_url(), a method new in 4.7.7. The method’s docblock says @since 1.0.32, the ZipWP Images version in this release:

public function is_allowed_image_url( $url ) {
	$allowed_hosts = apply_filters(
		'zipwp_images_allowed_hosts',
		array(
			'images.pexels.com',
			'cdn.pixabay.com',
			'pixabay.com',
			'images.unsplash.com',
			'plus.unsplash.com',
		)
	);

	$host = wp_parse_url( (string) $url, PHP_URL_HOST );

	if ( is_string( $host ) && in_array( $host, (array) $allowed_hosts, true ) ) {
		return true;
	}

	return false;
}

wp_parse_url() with PHP_URL_HOST returns the host part of the URL and nothing else. For a URL without a host, wp_parse_url() returns null. For a URL that PHP cannot parse, the function returns false. The is_string() test rejects both. in_array() runs in strict mode, because the third argument is true. So the host must equal one list entry, character for character. A different subdomain of pixabay.com fails the test unless the list names that subdomain. An unknown, missing or broken host makes the method return false. The handler then stops with the error “Invalid image URL.”

The second handler sits in the Astra_Sites class, in inc/classes/class-astra-sites.php. The handler’s docblock reads “Download and save the image in the media library.” The code has the same shape as the ZipWP handler. Release 4.7.7 adds the same kind of guard after the empty test:

 				wp_send_json_error( __( 'Need to send URL of the image to be downloaded', 'astra-sites' ) );
 			}
 
+			if ( ! astra_sites_is_valid_url( $url ) ) {
+				wp_send_json_error( __( 'Invalid image URL.', 'astra-sites' ) );
+			}
+
 			$image  = '';
 			$result = array();

astra_sites_is_valid_url() already exists in 4.7.6, and 4.7.7 leaves the function unchanged. The diff does not show the function’s body. Release 4.7.7 feeds new hosts into the function through a filter instead. Next to the plugin’s other add_filter() calls, the release registers one method on two filters:

add_filter( 'astra_sites_valid_url', array( $this, 'add_valid_image_hosts' ) );
add_filter( 'zipwp_images_allowed_hosts', array( $this, 'add_valid_image_hosts' ) );

add_valid_image_hosts() merges five hosts into the list each filter passes in. The five are images.pexels.com, cdn.pixabay.com, pixabay.com, images.unsplash.com and plus.unsplash.com. The method’s docblock gives the reason: both handlers “share one list”. That docblock implies astra_sites_is_valid_url() reads a host list through the astra_sites_valid_url filter. The diff shows neither the hosts that list held before nor how the function compares them. So this post cannot say whether the Astra_Sites handler matches hosts as strictly as is_allowed_image_url().

Before 4.7.7, a caller who reached either handler chose the host the WordPress server downloaded from. That host could sit on the server’s private network, out of reach from the internet. The request came from the web server itself. So an internal service that trusts the web server received a request from a machine it trusts. The two docblocks describe a download that ends up in the media library. The diff does not show what happens to a response that is not an image. So the diff shows an attacker who can aim the request. The diff does not show the attacker reading the response back.

The second fix changes one capability name in four places. Three of the handlers live in Astra_Sites_Importer, in inc/classes/class-astra-sites-importer.php. The CartFlows hunk shows the pattern:

 				// Verify Nonce.
 				check_ajax_referer( 'astra-sites', '_ajax_nonce' );
 
-				if ( ! current_user_can( 'edit_posts' ) ) {
+				if ( ! current_user_can( 'manage_options' ) ) {
 					wp_send_json_error( __( "Permission Denied: You don't have permission to import CartFlows flows. Please contact your site administrator.", 'astra-sites' ) );
 				}

The Cart Abandonment Recovery and LatePoint hunks differ only in the error text. The fourth handler sits in Astra_Sites near line 958 and replies with a bare wp_send_json_error(). The vendor’s changelog calls that handler the template data request endpoint. The diff does not show the function name or AJAX action of any of the four handlers.

Each handler first calls check_ajax_referer() with the action astra-sites and the field _ajax_nonce. A missing or wrong nonce ends the request there. Next, the handler calls current_user_can(). In 4.7.6 the handler asked for edit_posts. WordPress roles give edit_posts to Contributors, Authors, Editors and Administrators by default. In 4.7.7 the handler asks for manage_options, which only Administrators hold by default.

A nonce does not check a role. WordPress ties each nonce to one user session and one action. Any logged-in user holds a valid astra-sites nonce once a page they can open prints one. The diff does not show which pages print that nonce. So in 4.7.6, a Contributor with that nonce passed both checks. The error strings name the work behind the checks: import of CartFlows flows, Cart Abandonment Recovery data and LatePoint data. The diff does not show the import code, so the exact effect of each import is not established here.

Who is exposed

Sites on Starter Templates 4.7.6 carry both flaws. This post compares 4.7.6 with 4.7.7 only. The diff does not show which older release first shipped each handler.

For the two sideload handlers, the diff does not show the hook, the nonce check or the capability check. So this post cannot name the lowest role that reaches them. The four other handlers in the diff check the astra-sites nonce. The diff does not show whether the sideload handlers do the same.

The SSRF impact depends on what the web server can reach. An attacker gains more when the server sits next to internal admin panels, databases or cloud services. A lone server on shared hosting gives an attacker less to reach.

The capability flaw needs a logged-in account with edit_posts and a valid astra-sites nonce. By default that means Contributor or higher. Default WordPress registration gives new users the Subscriber role, which lacks edit_posts. A site where only administrators log in has no exposure to this part of the fix. Sites with guest authors, outside writers or a shared editorial team carry the exposure. The diff does not show whether CartFlows, Cart Abandonment Recovery or LatePoint must be active for an import to run.

What to do

Update Starter Templates to 4.7.7. In 4.7.7, both image sideload handlers refuse a URL whose host is not on the allowlist. The handlers reply “Invalid image URL.” The four import and data handlers now require manage_options. If you already updated, the checks below cover the time before your update.

  • Open Users and list every Contributor, Author and Editor. Remove or downgrade accounts you do not recognise.
  • If CartFlows, Cart Abandonment Recovery or LatePoint run on the site, look for flows, records or settings that no administrator imported.
  • Look through recent media library items. A file that holds text, HTML or JSON instead of a picture needs a closer look.
  • If you keep logs for internal services, look for requests from the WordPress server before the update.
  • Search your theme and plugins for code that uses the zipwp_images_allowed_hosts or astra_sites_valid_url filters. Any host added there passes the new allowlist, so only public stock image hosts belong on that list.

More research

Segurium is a WordPress malware scanner and cleanup tool on the official directory at wordpress.org/plugins/segurium/.