SECURITY ADVISORY / 01

CVE-2025-14351 Exploit & Vulnerability Analysis

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

cve_patchdiff:custom-fonts NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

Unauthenticated attackers can delete the plugin's local font cache simply by visiting the plugin admin page slug.

curl -i -s 'https://target.example.com/?page=bsf-custom-fonts' -o /dev/null -w '%{http_code}\n'

This request hits the vulnerable constructor in BCF_Google_Fonts_Compatibility. The attacker observes a normal HTTP response, and behind the scenes the plugin deletes the fonts directory and rewrites theme.json without requiring login or a nonce.

What the Patch Did

Before:

public function __construct() {
    if ( empty( $_GET['page'] ) || BSF_CUSTOM_FONTS_ADMIN_PAGE !== $_GET['page'] ) {
        return;
    }

    $bcf_filesystem    = bcf_filesystem();
    $fonts_folder_path = $this->get_fonts_folder();

    if ( file_exists( $fonts_folder_path ) ) {
        $bcf_filesystem->delete( $fonts_folder_path, true, 'd' );
    }

    self::delete_all_theme_font_family();
    add_action( 'admin_init', array( $this, 'update_fse_theme_json' ) );
}

After:

public function __construct() {
    add_action( 'admin_init', array( $this, 'maybe_rebuild_fonts' ), 10 );
}

public function maybe_rebuild_fonts() {
    if ( empty( $_GET['page'] ) || BSF_CUSTOM_FONTS_ADMIN_PAGE !== $_GET['page'] ) {
        return;
    }

    if ( ! isset( $_GET['bcf_rebuild_fonts'] ) ) {
        return;
    }

    // Security: Capability and nonce checks.
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( 'You do not have permission to perform this action.', 'custom-fonts' ), 403 );
    }

    if ( ! isset( $_GET['bcf_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['bcf_nonce'] ) ), 'bcf_google_fonts_rebuild' ) ) {
        wp_die( esc_html__( 'Security check failed. Please try again.', 'custom-fonts' ), 403 );
    }

    $bcf_filesystem    = bcf_filesystem();
    $fonts_folder_path = $this->get_fonts_folder();

    if ( file_exists( $fonts_folder_path ) ) {
        $bcf_filesystem->delete( $fonts_folder_path, true, 'd' );
    }

    self::delete_all_theme_font_family();
    $this->update_fse_theme_json();

    $redirect_url = add_query_arg(
        array(
            'page'          => BSF_CUSTOM_FONTS_ADMIN_PAGE,
            'fonts_rebuilt' => '1',
        ),
        admin_url( 'themes.php' )
    );

    wp_safe_redirect( $redirect_url );
    exit;
}

The patch added an explicit admin-init hook, a bcf_rebuild_fonts trigger guard, a current_user_can('manage_options') authorization check, and a wp_verify_nonce() verification.

Root Cause

This is an authorization bypass / CSRF-style bug (CWE-862 / CWE-352) caused by destructive behavior running directly in a constructor with only $_GET['page'] as a guard. The attacker controls $_GET['page']; if it matches BSF_CUSTOM_FONTS_ADMIN_PAGE, the plugin immediately calls the filesystem delete path and updates theme JSON. No capability check or nonce validation separates the attacker from the sink, so the request crosses the trust boundary from any visitor into privileged plugin cleanup logic.

Why It Works

The vulnerability exists because the destructive code executes before any security gate is applied. In the original constructor, the only check is:

if ( empty( $_GET['page'] ) || BSF_CUSTOM_FONTS_ADMIN_PAGE !== $_GET['page'] ) { return; }

That means any request with the right page value reaches the sink. The single load-bearing fix is the current_user_can( 'manage_options' ) authorization check; without it, an unauthenticated or unauthorized visitor would still be able to trigger deletion. The nonce check is added for CSRF protection and to ensure the action is only performed from a valid admin workflow, while moving the process to admin_init and requiring bcf_rebuild_fonts prevents the side effect from happening during normal object construction.

Hardening Checklist

  • Use current_user_can( 'manage_options' ) or the least privileged capability required before any filesystem mutation.
  • Protect state-changing admin actions with wp_verify_nonce() and generate URLs with wp_nonce_url().
  • Avoid doing destructive work in plugin constructors; defer to action hooks such as admin_init.
  • Require an explicit action trigger like isset( $_GET['bcf_rebuild_fonts'] ) before performing cleanup.
  • After completing admin actions, redirect with wp_safe_redirect() and exit to avoid duplicate execution.

References

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

Frequently asked questions about CVE-2025-14351

What is CVE-2025-14351?

CVE-2025-14351 is a security vulnerability. 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-14351?

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

How does CVE-2025-14351 get exploited?

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

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

CVE-2025-14351 — 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-14351?

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

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

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