Aryan Jasala

When Cloudflare Sneezed and WordPress Caught a Fatal Error

A break between the edge and the origin, ending in a fatal error

A Cloudflare outage on June 13 took down wp-admin on a client site that does not use Cloudflare. The cause was a Yoast license check that received a 404-shaped response, treated it as valid, and cached it. Below is the stack trace, the line that broke, and the PR.

How a Cloudflare outage reached wp-admin
Cloudflare outagelicense endpoint goes dark
License checkruns on admin_init, every load
404 cachedstored as a valid response
array_map on nullsubscriptions field arrives null
wp-admin fatalno dashboard, no plugins page

The site had zero DNS records at Cloudflare. The dependency arrived through the plugin.

The SEO plugin's license check ran through Cloudflare on every admin page load, cached the outage's 404 as a valid response, and took wp-admin down.

The error

Uncaught Error: array_map(): Argument #2 ($array) must be of type array, null given

Stack trace landed in wordpress-seo/inc/class-addon-manager.php on line 820. No dashboard, no plugins page, so no way to switch off the plugin from the screen the plugin had just taken away. 🙃

Disabling Yoast cleared it instantly.

Why a site not on Cloudflare went down

The site had 0 DNS records pointing at Cloudflare. Yoast does.

Yoast Premium validates its license against https://my.yoast.com/api/sites/current, and it makes that call on admin_init. That endpoint sits behind Cloudflare. So the site never touched Cloudflare and still inherited the outage, through a plugin, on every admin page load. ☁️

What the API returned

{ "statusCode": 404, "error": "Not Found", "message": "No sites found" }

The plugin treated it as a valid response, then cached it. It asked a question, got a 404, wrote the 404 down and committed to it. One poisoned response broke every Yoast-enabled site on the machine until that cache cleared. ☠️

Where it blew up

'subscriptions' => array_map(
    [ $this, 'map_subscription' ],
    $site_information->subscriptions
),

$site_information->subscriptions was null. Since PHP 8.0 that is a TypeError and not a warning, so it stops being a log line and starts being a white screen. 💥

The four-line fix and its limits

Before

  • Response used without any checks
  • array_map() runs over null subscriptions
  • PHP 8 raises a TypeError
  • Every admin page load fatals

After the fix

  • isset() on url and subscriptions
  • is_array() check on subscriptions
  • Falls back to default site information
  • Bad response still cached

The strpos( $response_code, '200' ) check is also still in the same function.

The isset() guard stops the fatal error, but the poisoned cache and the substring status-code check survive it.

The status code check

Same file, while reading around:

if ( $response_code === 200 || strpos( $response_code, '200' ) !== false ) {

Cloudflare runs its own status codes in the 1200 range. 1200 contains 200, so it passes. Somewhere a Cloudflare error is being read as a valid license check and nobody in the transaction is aware of it. 🤡

Not the root cause here, but it is in the same function.

The fix

PR: https://github.com/Yoast/wordpress-seo/pull/22353

$response = $api_request->get_response();

if ( isset( $response->url, $response->subscriptions ) && is_array( $response->subscriptions ) ) {
    return $response;
}

return $this->get_site_information_default();
  • checks url and subscriptions exist
  • checks subscriptions is actually an array
  • falls back to defaults when the response is incomplete

Four lines of isset() against one afternoon of no wp-admin.

What this does not cover

The cache. A bad response is still stored, it just no longer takes wp-admin down with it. The strpos( $response_code, '200' ) check is also still there.

Worth repeating

  • a license check should never be able to fatal a CMS
  • array_map() is not validation
  • an external API is going to hand you something you did not plan for, and a plugin running on admin_init has to survive it

Cloudflare going down is not preventable. A plugin taking wp-admin down with it is. 🧯