WordPress 3.7 was released on October 24, 2013 and introduced an automatic update mechanism to ensure security fixes would be automatically deployed on all WordPress sites, in an effort to prevent recently-patched vulnerabilities from being massively exploited in the wild. This is widely regarded by security experts as a good idea.
However, the WordPress automatic update feature had one glaring Achilles' heel: If a criminal or nation state were to hack into the WordPress update server, they could trigger a fake automatic update to infect WordPress sites with malware.
This isn't just a theoretical concern, it could have happened if not for WordFence's security researchers finding and disclosing an easy attack vector into their infrastructure.
WordPress 5.2 was released on May 7, 2019 and provides the first real layer of defense against a compromised update infrastructures: offline digital signatures.
Recommended reading: What's a digital signature?
Signature Verification in WordPress 5.2
When your WordPress site installs an automatic update, from version 5.2 onwards, it will first check for the existence of an x-content-signature
header. If one isn't provided by the update server, your WordPress site will instead query for a filenamehere.sig
file.
No matter how it's delivered, the signatures are calculated using Ed25519 of the SHA384 hash of the file's contents. The signature is base64-encoded for safe transport.
The signing keys used to release updates are managed by the WordPress.org core development team. The verification key for the initial release of WordPress 5.2 is fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=
(expires April 1, 2021).
(For the sake of specificity: Signing key here means Ed25519 secret key, while verification key means Ed25519 public key.)
With the necessary information in hand, your WordPress site will then verify that the signature is valid. You can check this yourself, like so:
<?php
require_once "vendor/autoload.php"; // for Composer autoloaders, sodium_compat, etc.
function isWordPressUpdateValid(string $signature, string $filename): bool
{
$public_key = base64_decode('fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=');
$hash = hash_file('sha384', $filename, true);
return sodium_crypto_sign_verify_detached(base64_decode($signature), $hash, $public_key);
}
WordPress 5.2 users can just use the built-in verify_file_signature()
function.
For the first release, WordPress will (by default) soft-fail if the signature is not valid. In future releases, the default will be configured to a hard failure. The reason for this unsafe default is to ensure updates aren't blocked if there's a bug in the update code (i.e. arithmetic bugs with PHP 7.2.0-7.2.2, without ext/sodium, with PHP-FPM and opcache enabled-- kind of a narrow corner-case, and hopefully the only one, but better safe than sorry).
If you're running a version of PHP older than 7.2, the Ed25519 signature verification is provided by sodium_compat, a pure-PHP polyfill of libsodium developed by us.
To learn more about how we built sodium_compat, check out Cryptographically Secure PHP Development.
What All of This Means for WordPress Security
Before WordPress 5.2, if you wanted to infect every WordPress site on the Internet (approximately 33.8% of websites as of this writing), you just had to hack their update server. Upon doing so, you can trick the automatic update feature into downloading and installing arbitrary code, which allows you to do all sorts of nefarious things (e.g. build the world's largest DDoS botnet).
After WordPress 5.2, you would need to pull off the same attack and somehow pilfer the signing key from the WordPress core development team.
Bonus: Modern Cryptography for WordPress Plugin Developers
In addition to the security enhancements to the WordPress core, the inclusion of sodium_compat on WordPress 5.2 means that plugin developers can start to migrate their custom cryptography code away from mcrypt (deprecated in PHP 7.1, removed in PHP 7.2) and towards libsodium (introduced in PHP 7.2, polyfilled by sodium_compat).
Libsodium has a lot of features, but plugin developers might be most interested in how to seamlessly and securely migrate to the new APIs.
How to Seamlessly and Securely Upgrade your Plugins to Use the New Cryptography APIs
Strategy 1: All Data Decryptable at Run-Time
If you can encrypt/decrypt arbitrary records, the most straightforward thing to do is to use mcrypt_decrypt()
to obtain the plaintext, then re-encrypt your code using sodium_crypto_secretbox()
in one sitting. Then remove the runtime code for handling mcrypt-encrypted messages.
Strategy 2: Only Some Data Decryptable at Run-Time
We've covered legacy hash migrations in our guide to secure password storage. The same idea applies here:
If you can't decrypt all records at once, the best thing to do is to immediately re-encrypt everything using sodium_crypto_secretbox()
and then, at a later time, apply the mcrypt-flavored decryption routine (if it's still encrypted).
Avoid Opportunistic Upgrades. It may be tempting to decrypt then re-encrypt rows opportunistically, but it's important to learn from history. Yahoo made this mistake, and as a result, had lots of MD5 password hashing lying around their database when they were breached, even though their active users had long since upgraded to bcrypt.
Don't be a yahoo. Re-encrypt all your data up front.
The Future: Code-Signing for All Developers
The work that went into WordPress 5.2 started many years ago and only covers core updates. Themes and plugins are still not cryptographically signed.
In the future, we will be working to implement a system that allows vendors to sign their own releases and publish these signatures (and related metadata) to an append-only cryptographic ledger. Once this is done, WordPress's auto-update will finally be secure.
The system we designed is called Gossamer and will be the subject of our next WordPress core ticket, possibly targeting 5.3 or 5.4 (depending on how much breaks with 5.2).
If WordPress's goal is to democratize publishing, then Gossamer's goal is to democratize code-signing.