Severity Daily

IT and AI security incidents, checked against the primary source

Tag: PHP

  • Hummingbird’s debug-log guard checked for a class in the wrong namespace, and an unauthenticated cookie name became executable PHP

    Hummingbird’s debug-log guard checked for a class in the wrong namespace, and an unauthenticated cookie name became executable PHP

    A guard that was written, shipped, and never once executed: the header meant to make Hummingbird’s cache debug log inert was gated on a class name PHP could never resolve, and the plugin then wrote unsanitized cookie names into the file.

    What happened

    CVE-2026-83627 reached the National Vulnerability Database at 6:17 a.m. UTC on Friday, September 5, 2026, assigned by Wordfence, scored 9.8 with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, and filed under CWE-94, improper control of generation of code. Its vulnStatus is Received. It carries no CISA KEV fields and no federal deadline. The affected product is Hummingbird, WPMU DEV’s page-speed and caching plugin, in all versions up to and including 3.21.0. The fix is 3.21.2. The WordPress.org listing for the free plugin reports “70,000+” active installations.

    The record is unusually specific about the mechanism, and the mechanism is worth reading in full because every step of it is ordinary.

    Hummingbird’s page-cache debug log is written to wp-content/wphb-logs/page-caching-log.php. That is a directly web-accessible file with a .php extension, which is a deliberate and common design: the file is supposed to open with a leading <?php die(); ?> header, so that anyone who requests it over the web gets nothing, while the plugin itself reads past the header. It is a standard WordPress idiom and it works fine when the header is there.

    The header was gated on class_exists( 'Filesystem' ). In Wordfence’s words, that check “can never match because class_exists() resolves string arguments in the global namespace while the class is Hummingbird\Core\Filesystem.” The string 'Filesystem' is not the class’s fully qualified name, so the test returns false every time, in every installation, for every version. When the log file was created during a front-end request, the protective header was “omitted entirely.”

    What then goes into the file is the second half. The plugin’s get_cookies() writes “the raw name of any cookie matching the wphb_cache_ prefix into that file without sanitization.” A cookie name is attacker-controlled, arrives on an anonymous request, and needs no account. Set a cookie whose name begins with wphb_cache_ and continues with PHP, make one request, then request page-caching-log.php directly. The file executes.

    Two preconditions keep this from being universal, and both belong in the first paragraph of anyone’s assessment. The site administrator must have enabled Page Caching with the Debug Log option, which Wordfence explicitly labels non-default. And the log file must have been created during a front-end request rather than an admin one. That second condition sounds like a narrow window until you read how it is reached: the plugin’s own “Clear logs” action, any cache flush, or — and this is the part that matters — the plugin’s daily log-rotation cron, which Wordfence says “can strip the protective header from an existing log file.”

    The fix shipped in changeset 3675836 to the plugin’s trunk on Tuesday, September 1, 2026, at 10:16 a.m., with the commit message “3.21.2 trunk.” It rewrites the guard so that the header is verified on every write rather than only when the file is absent: the new code computes whether the file is missing or does not already start with the guard prefix, and it returns without writing if the filesystem helper is unavailable. It does not change what get_cookies() writes. The defense is still that the file begins with die(); the change is that the plugin now checks that this is true each time instead of assuming it.

    Why it matters

    The interesting failure here is not that a check was missing. A check was written. Someone thought about the log file being web-accessible, decided it needed a guard, wrote the guard, and wrapped it in a conditional that was wrong in a way no test would catch and no code review would notice. class_exists( 'Filesystem' ) reads correctly in English. It is false in PHP, silently, forever, and the only symptom is a file that quietly lacks a header nobody was going to look at.

    This publication has now covered three variations on the same shape in one week. python-jose’s 2024 patch checked the key format rather than the key type, so a public key could still serve as an HMAC secret. Kestra’s authentication filter called endsWith on a path, which is not the same question as whether the path is an auth-exempt config route. goose’s recipe security scanner ran over every field except the two that execute commands. In each case a security control exists, is visible in the source, satisfies a reviewer reading for intent, and tests something adjacent to the thing that matters. These are not the same bug, but they fail the same way: the control’s presence is what gets audited, and its semantics are what break.

    The practical lesson is narrower than “write better tests,” because most of these would survive a unit test written by the same person who wrote the check. What catches them is asserting the outcome rather than the mechanism — not “does the guard branch run” but “does an HTTP request for this file return nothing.” A single integration test that fetched page-caching-log.php over HTTP and asserted an empty body would have failed on day one of this bug’s existence, in every version, without anyone needing to understand namespace resolution.

    The log-rotation cron deserves its own note, because it inverts the usual reasoning about non-default settings. Normally a non-default configuration is a filter that shrinks the affected population and keeps shrinking it: the fewer people who turned it on, the smaller the problem. Here, among sites that did turn Debug Log on, the vulnerable state is not something an administrator has to walk into. It arrives on a timer. A site that was protected because its log file happened to be created in an admin context becomes unprotected the next time rotation runs. “We enabled that once for troubleshooting and forgot about it” is the exact population at risk, and it is a population that will not remember to check.

    On scale, be precise rather than dramatic: 70,000-plus is WordPress.org’s active-install figure for the free plugin, it is a rounded band rather than a measurement, and the vulnerable subset is only those installations with Page Caching and Debug Log both on. Nobody knows that number, including Wordfence. There is no evidence of exploitation from anyone, and the CVE carries no KEV listing. What makes this worth acting on quickly is not that it is being exploited; it is that the fix is now public in a readable trac diff against a plugin whose source is downloadable by anyone, and the exploitation path is one cookie and two HTTP requests.

    What to do

    Update Hummingbird to 3.21.2 or later. Versions through 3.21.0 are affected; 3.21.2 is the fixed release.

    Before or immediately after updating, check whether you were in the vulnerable configuration and whether anything used it. In the Hummingbird settings, look at whether Page Caching is enabled and whether the Debug Log option under it is on. Then look on disk: if wp-content/wphb-logs/page-caching-log.php exists, open it and read the first line. If it does not begin with a PHP header that terminates execution, that file was web-readable, and its contents were served to anyone who asked. Search it for anything resembling PHP tags, and for cookie names beginning with wphb_cache_ that do not look like cache keys.

    Treat a hit there as a possible code-execution event, not as a logging oddity: check for recently modified or unfamiliar PHP files across the webroot, review your access logs for direct requests to page-caching-log.php, and rotate credentials that a web shell would have reached. Then turn the Debug Log option off. It is a troubleshooting setting, and the safest state for a diagnostic log that writes attacker-controlled strings into an executable file extension is not running.

    Sourcing note

    CVE-2026-83627 was read from the NVD API by CVE ID; the description quoted here is Wordfence’s, reproduced from that record, and the published timestamp, CVSS vector, CWE, and absence of CISA KEV fields come from the same retrieval. The fix was checked against changeset 3675836 on plugins.trac.wordpress.org, which supplied the September 1, 2026 timestamp, the commit message, and the substance of the diff. The active-install figure and the current version are WordPress.org’s own, read from the plugin’s listing page.

    Not reached: Wordfence’s own threat-intelligence page for this vulnerability returned no readable content on two attempts, so the researcher credit and Wordfence’s disclosure timeline are not reported here. WPMU DEV has published no standalone advisory that this desk located, and was not contacted. This desk did not test the vulnerability, and the exploitation description above is Wordfence’s account of it rather than an independent reproduction.