Blocksy Companion CVE-2026-15158: Double Extension Upload Bypass
By WP Vanguard Team
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:
// 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 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, 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.woff2or.ttfas a substring but don't end in them.shell.woff2.phpis 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 and the 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
Related reading
Check Your WordPress Site Security
Free scan, no login required. Find vulnerabilities before attackers do.
Scan Your Site FreeGet weekly WordPress security tips
Vulnerability alerts, plugin updates, and security guides. No spam. Unsubscribe any time.