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 withwp_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()andexitto avoid duplicate execution.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14351