---
title: "Blocksy Companion CVE-2026-15158: Double Extension Upload Bypass"
slug: "blocksy-companion-double-extension-upload-cve-2026-15158"
description: "A strpos() substring check in Blocksy Companion let shell.woff2.php pass as a font, opening unauthenticated file upload on 2.1.46 and below."
date: 2026-07-04
author: "WP Vanguard Team"
category: "Research"
tags: ["double-extension-file-upload-bypass", "cve-2026-15158", "blocksy-companion", "arbitrary-file-upload", "wp-check-filetype-and-ext"]
featured_image: "/images/blog/blocksy-companion-double-extension-upload-cve-2026-15158.jpg"
---

CVE-2026-15158 carries a CVSS score of 9.8 and needs no authentication to exploit. The bug lives in Blocksy Companion's `save_attachments` function, in every version up to and including 2.1.46, and it comes down to one line of validation logic that checked the wrong thing. The plugin approved a filename as a font file if `.woff2` or `.ttf` showed up anywhere in the string. `shell.woff2.php` qualifies. So does `evil.ttf.php`. The fix shipped in 2.1.47, and the flaw was disclosed on 1 July 2026.

This is a double extension file upload bypass, and it's worth studying on its own because the root cause isn't a missing check. A check was there. It just answered the wrong question. That distinction matters for how you review code, because "there's validation here" is not the same claim as "the validation is correct," and a lot of code review stops at the first one.

## How the flaw works

Blocksy Companion's Custom Fonts extension needs to let site owners upload `.woff2` and `.ttf` files so custom typefaces work. To do that, it hooks into `wp_check_filetype_and_ext`, the WordPress filter that every file upload passes through before WordPress decides what MIME type to assign and whether to allow the upload at all. The extension's hook looks at the filename and, if it contains one of the allowed font substrings, tells WordPress the file is safe.

The mechanism is a `strpos()` call, or logic equivalent to it: search the filename string for `.woff2` or `.ttf`, and if the substring is found anywhere, approve it. That's a containment check, not a position check. It never asks where in the string the match occurred.

A real font file's extension sits at the end: `myfont.woff2`. But `strpos('shell.woff2.php', '.woff2')` also returns a match, because `.woff2` is sitting in the middle of the string. The actual final extension is `.php`. Nothing in the validator ever looked at the final extension. It just asked "does this text appear somewhere in here," and a filename can contain any substring an attacker wants while still ending in whatever executable extension the server will run.

The correct approach is to resolve the real extension first, then compare it against an allowlist:

```php
// Wrong: substring check anywhere in the filename
if (strpos($filename, '.woff2') !== false || strpos($filename, '.ttf') !== false) {
    $approved = true;
}

// Right: resolve the actual final extension, then allowlist it
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$allowed = ['woff2', 'ttf', 'woff', 'otf'];
if (in_array($ext, $allowed, true)) {
    $approved = true;
}
```

`pathinfo($filename, PATHINFO_EXTENSION)` returns everything after the last dot: for `shell.woff2.php` that's `php`, not `woff2`. Compared against a hardcoded allowlist, that filename gets rejected. The substring version never had a chance of catching it, because it wasn't measuring extension at all. It was measuring presence.

This is the general rule for any filename or path validation in PHP: resolve the value you actually care about first, then compare it, rather than testing whether a fragment of the raw string shows up somewhere. The same mistake shows up with `strpos()` checks against full URLs (matching a trusted domain as a substring instead of parsing the host), against SQL identifiers, and against uploaded MIME types reported by the browser instead of the file's real signature. Substring matching is cheap to write and passes every test case a developer thinks to try, because the developer is testing the honest input, not the adversarial one.

The entry point for the malicious request is the `blc-review-images[]` parameter, part of the same upload handler the Custom Fonts extension shares with the plugin's WooCommerce review-image feature. An unauthenticated request that reaches `save_attachments` with a crafted filename in that array gets waved through by the font check, and the file lands on disk with a `.php` extension a web server will happily execute. Nothing about the review-image feature itself is broken; it's collateral damage from a filter it never asked to depend on.

## Who is affected

Every install of Blocksy Companion running 2.1.46 or earlier is vulnerable, on the version that carries the Custom Fonts extension's `wp_check_filetype_and_ext` hook. Blocksy is a widely used theme companion plugin, which means the exposure isn't limited to sites that deliberately opted into some advanced feature. If the plugin is active at 2.1.46 or below, the filter is registered and the substring check is live for that install.

No special configuration is required beyond having the plugin active at an affected version. It's an unauthenticated vulnerability: no login, no capability, no user interaction. That's what pushes the CVSS score to 9.8. An attacker doesn't need an account on the site, doesn't need to trick an admin into clicking anything, and doesn't need the site to be misconfigured in some unusual way. The vulnerable code path is reachable from a plain HTTP request.

Because the flaw sits in a filter hooked globally rather than in code gated behind a specific admin screen, there's also no way to tell from the outside whether a given site is exploitable just by checking whether it "uses" custom fonts. If the extension's code is loaded and the plugin version is 2.1.46 or earlier, the filter is registered on every page load, whether or not the site owner ever opened the font settings panel.

## The real design smell: a font feature editing global validation

The substring-versus-extension bug is the proximate cause, but there's a second problem sitting underneath it that's worth calling out, because it's a pattern that shows up across a lot of plugin code, not just this one.

`wp_check_filetype_and_ext` is not a Custom-Fonts-only filter. It's the single global gate WordPress runs every upload through, regardless of which plugin, which admin screen, or which REST endpoint initiated it. When Blocksy Companion's Custom Fonts extension hooked into it to add `.woff2` and `.ttf` support, it wasn't adding a rule that only applies to its own font uploader. It was widening what the entire site's upload pipeline considers valid, for every upload path on the install, including ones the extension's author never thought about, like the `blc-review-images[]` handler.

That's the design smell. A feature scoped to "let site owners add custom fonts in one settings screen" ended up touching a security-critical global filter that governs uploads across the whole plugin ecosystem installed on that site. The blast radius of the bug wasn't "font uploads got weaker." It was "every upload path that eventually funnels through `wp_check_filetype_and_ext` inherited the substring check's blind spot."

When you're reviewing a hook into `wp_check_filetype_and_ext`, `upload_mimes`, or similar global filters, the question to ask isn't just "does this validation logic work for the case I'm adding." It's "what else routes through this filter, and does my change weaken it for paths I didn't write." A font uploader and a review-image uploader had no business sharing a validation bug, but they did, because the validation lived in a global filter instead of a function scoped to the feature that needed it. If a feature only needs to accept two more file types for its own narrow upload form, validate them locally in that form's handler. Don't reach into the filter every other plugin's uploads pass through.

This same shape shows up in other advisories. Attackers look for [PHP backdoors](/blog/php-backdoors-in-wordpress) uploaded through exactly this kind of gap, and file-upload bypasses keep landing on the WordPress vulnerability feeds because the underlying mistake (validate the trusted case, forget the adversarial one) is easy to make and easy to miss in review. We covered a related upload-validation failure in the [Ninja Forms file upload vulnerability](/blog/ninja-forms-file-upload-vulnerability), where a different field-level check made the same category of mistake with a different mechanism.

## What to do now

Update Blocksy Companion to 2.1.47 or later. That's the fixed version, and it's the only complete remediation. There's no configuration workaround, because the vulnerable filter runs regardless of settings, as long as the Custom Fonts extension is present in an affected version.

If your site ran an affected version before you patched, check for these indicators of compromise:

- Any file in `wp-content/uploads/` with a double extension ending in `.php`, `.phtml`, `.php5`, or similar, especially filenames that contain `.woff2` or `.ttf` as a substring but don't end in them. `shell.woff2.php` is the pattern named in the advisory, but a real attacker will vary the shell name.
- Recently modified files in the uploads directory that aren't images, fonts, or media your site normally stores there. Uploads directories should hold static assets, not executable PHP.
- Unexpected admin users or role changes created after the disclosure window, since a working file upload frequently gets chained into a webshell that then creates a persistent backdoor account.
- Outbound requests from your server to unfamiliar IPs, which can indicate an uploaded shell is being used to exfiltrate data or pivot to other sites on shared hosting.

If you find a suspicious file, don't just delete it. Preserve a copy for analysis, check your access logs around its creation timestamp for the requesting IP, and look for exactly what that IP touched afterward, because a single uploaded shell rarely stays a single file for long. Our [guide to signs your WordPress site has been hacked](/blog/signs-wordpress-site-hacked) and the [WordPress malware removal guide](/blog/wordpress-malware-removal-guide) both walk through the full cleanup process if you find evidence of compromise rather than just an unpatched version.

WP Vanguard's scanner flags plugins running versions with known CVEs like this one, including the specific file-upload class this advisory falls under.

## References

- [Wordfence Threat Intelligence: CVE-2026-15158](https://www.wordfence.com/threat-intel/vulnerabilities/id/2df449b4-3f3b-4afc-b391-8d8d11710c07?source=cve)
- [NVD: CVE-2026-15158 Detail](https://nvd.nist.gov/vuln/detail/CVE-2026-15158)
- [Patchstack: Blocksy Companion Vulnerabilities](https://patchstack.com/database/wordpress/plugin/blocksy-companion)
- [Freshy Sites Security Bulletin: Blocksy Companion Plugin Vulnerability (CVE-2026-15158)](https://freshysites.com/security-bulletins/blocksy-companion-plugin-vulnerability-cve-2026-15158/)
