[Our previous post](https://paragonie.com/blog/2016/08/cms-airship-simply-secure-content-management-now-available-in-aws-marketplace) included a checklist comparing [CMS Airship](https://github.com/paragonie/airship) (our [Free Software](https://www.gnu.org/philosophy/free-sw.en.html) CMS platform designed with security in mind) to the [three most popular content management systems](https://w3techs.com/technologies/overview/content_management/all) currently in use on the Internet:
1. WordPress (26.6% of all websites)
2. Joomla (2.8% of all websites)
3. Drupal (2.2% of all websites)
The checklist compared out-of-the-box security properties (features or design decisions that affect the security of the software and any extensions developed for it) rather than what's possible with community-provided extensions. Tooltips were also provided on individual cells to clear up any confusion on why we did or did not award a checkmark to a given project for a given security property.
Since the previous post was published, several technologists asked us to explain the individual security deficits of other PHP content management systems in detail. Some of these are straightforward (e.g. WordPress doesn't offer encryption, so there's nothing to analyze), but others require a careful eye for code auditing. Familiarity with PHP security is also greatly beneficial to understanding, although we will attempt to explain each item in detail.
We're going to set Airship aside for the remainder of this post. All you need to know is Airship met all of the criteria for a secure-by-default content management system. If you'd like to learn more about [Airship's security features](https://paragonie.com/blog/2016/06/php-security-platinum-standard-raising-bar-cms-airship), we've covered this in detail [here](https://paragonie.com/blog/2016/05/keyggdrasil-continuum-cryptography-powering-cms-airship).
WordPress, Joomla, and Drupal: The Good Parts
All three content management systems score points for being Free Software, released under the GNU Public License. Consequently, their source code is available for their users to inspect and analyze. This offers three benefits:
1. Independent security experts can assess the security of their offering and, with source code citations to prove their arguments, explain what's secure or insecure.
2. Independent security experts can take their findings and offer better ways to improve the security of their software.
3. You have the ability to run a copy of the software that you've verified to be known-good.
For example, last year, we made WordPress's `wp_rand()` function cryptographically secure [as of WordPress 4.4.0](https://paragonie.com/blog/2015/10/coming-wordpress-4-4-csprng). This would not have been possible without the first two properties.
In addition to being open source, all three provide a security mechanism to mitigate [Cross-Site Request Forgery](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) attacks. We didn't include whether or not plugins/extensions fail to utilize the CSRF mitigation feature in our analysis. If you're using a third-party plugin, **don't assume that CSRF vulnerabilities can't or won't happen to your application just because there's a mitigation feature in the core.**
### Drupal: Context-Aware Output Escaping
The correct way to [prevent cross-site scripting vulnerabilities](https://paragonie.com/blog/2015/06/preventing-xss-vulnerabilities-in-php-everything-you-need-know) is to **escape data on output, not on input**.
Escaping on input can lead to bizarre exploitation strategies, e.g. [WordPress's stored XSS vulnerability enabled by MySQL column truncation](https://klikki.fi/adv/wordpress2.html).
You should be saving the original, unaltered copy of any data in case you need to update your escaping strategy to prevent a filter bypass. Escaping on output allows you a measure of agility that input escaping does not. You may, however, cache the escaped data for subsequent requests to improve your application's performance.
The latest versions of Drupal got this right, and should be commended for it.
### Joomla: Secure Password Storage and Two-Factor Authentication
Out of the three, Joomla is the only CMS that leverages [PHP's native password hashing features](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016#php). This means that cracking the passwords stored in a modern Joomla app is nontrivial, should one ever be compromised.
Additionally, Joomla now provides [two-factor authentication out-of-the-box](https://docs.joomla.org/J3.x:Two_Factor_Authentication), which helps mitigate the consequences of weak user passwords.
### Joomla: Secure PHP Encryption
In response to our security advisory about JCrypt's design flaws last year, the Joomla team adopted [Defuse Security's secure PHP encryption library](https://github.com/joomla/joomla-cms/blob/51badfe01e88b92d5cace58691e72ccda2d30166/libraries/php-encryption/Crypto.php) (version 1.2.1) instead.
While version 2 offers significant improvements over version 1, there are no known security vulnerabilities in that version of Defuse Security's PHP encryption library.
## The Bad and the Ugly
Security Deficits in WordPress's Core
WordPress Automatic Updates Are Not Secure
WordPress is the only one of the big three content management systems that offers automatic updates, but it does so insecurely.
In order to have secure automatic updates, you need to have a secure code delivery system in place. Secure code delivery has three properties:
1. **Cryptographic signatures**: The deliverable was signed by a private key and you can verify the signature with the corresponding public key.
2. **Reproducible builds**: You can reproduce the deliverable from the source code.
3. **Userbase consistency verification**: Everyone gets the same thing. Implementations involve append-only data structures, such as Merkle trees, which are also used in [certificate transparency](https://www.certificate-transparency.org) and Bitcoin.
WordPress's automatic updates are not cryptographically signed with (an offline) private key. This means if an attacker can compromise their update servers and upload a malicious download, they can install a trojan on 26.6% of the websites on the Internet. The consequences of a compromise of this magnitude cannot be understated. Such an attack could enable financial information fraud and distributed denial of service attacks on a scale we've never seen before, and that our systems are almost certainly incapable of enduring.
Nothing stops an attacker from silently distributing malware through a compromised update server only to targets of interest, since there are no userbase consistency verification protocols in place either.
However, given that WordPress is open source and PHP is an interpreted language, it's fair to give them credit for reproducible builds.
WordPress Does Not Use Prepared Statements
Many WordPress users are surprised to learn that WordPress doesn't use prepared statements, given the existence of [`wpdb::prepare()`](https://developer.wordpress.org/reference/classes/wpdb/prepare/).
To understand what's going on, you first need to know what prepared statements actually do:
1. The application sends the query string (with placeholders) to the database server.
2. The database server responds with a query identifier. Some servers allow you to cache and reuse query identifiers for multiple prepared queries to reduce round trips.
3. The application sends the query identifier and the parameters together to the server.
The reason this is a security boon over escape-then-concatenate is that the query string is never tainted by the parameters. They're sent in separate packets. This is an important distinction; [charset-based hacks to bypass `mysql_real_escape_string()`](http://stackoverflow.com/a/12118602/2224584) are dead on arrival when prepared statements are used.
What WordPress's `wpdb::prepare()` does instead of prepared statements is [escape-then-concatenate](https://github.com/WordPress/WordPress/blob/bd816e51823c11e072d5c3e013560ee480a887fd/wp-includes/wp-db.php#L1276-L1296).
WordPress Salted MD5 for Password Hashing
WordPress users may be surprised to learn that, despite using [Phpass](http://www.openwall.com/phpass/) (a well-regarded password hashing library written by Solar Designer which offered bcrypt before PHP got a native password hashing API), WordPress doesn't use bcrypt for password storage.
To understand why, first pay attention to [this code snippet](https://github.com/WordPress/WordPress/blob/c1e4d2535070069a2dd51e4da04fbf9c5e86b558/wp-includes/class-phpass.php#L233-L254) and [this one as well](https://github.com/WordPress/WordPress/blob/c1e4d2535070069a2dd51e4da04fbf9c5e86b558/wp-includes/class-phpass.php#L121-L164).
If `$this->portable_hashes` is set to `TRUE`, it will call `$this->crypt_private()` (which uses 8192 rounds of MD5).
Where is `$this->portable_hashes` defined? [In the constructor](https://github.com/WordPress/WordPress/blob/c1e4d2535070069a2dd51e4da04fbf9c5e86b558/wp-includes/class-phpass.php#L45-L53). And, of course, [it's always set to `TRUE`](https://github.com/WordPress/WordPress/search?utf8=%E2%9C%93&q=new+PasswordHash) when an object is created in the WordPress core.
Consequently, `HashPassword` can be greatly simplified to the following snippet:
function HashPassword($password)
{
if ( strlen( $password ) > 4096 ) {
return '*';
}
/* these steps are skipped */
$random = $this->get_random_bytes(6);
$hash =
$this->crypt_private($password,
$this->gensalt_private($random));
if (strlen($hash) == 34)
return $hash;
return '*';
}
[One reason for this deviation from Phpass](https://core.trac.wordpress.org/ticket/21022#comment:21) was to gracefully handle corner cases where someone downgrades to a version of PHP too old to support bcrypt without losing the ability to verify existing password hashes.
Security Deficits in Joomla's Core
Joomla Does Not Offer Automatic Updates
Joomla doesn't offer automatic security updates. In the event that a security vulnerability is discovered in Joomla and a fix is released, it's up to every individual Joomla site operator to validate and install the update manually. Until the patch is applied, your systems are vulnerable. As a consequence, [most Joomla websites still run outdated versions of Joomla](https://developer.joomla.org/about/stats.html).
Joomla Doesn't Provide Prepared Statements
To reiterate: Prepared statements are a way of interacting with a database that, among other things, makes [preventing SQL injection](https://paragonie.com/blog/2015/05/preventing-sql-injection-in-php-applications-easy-and-definitive-guide) simple while eliminating corner cases.
> Note: [Thanks to Mark Babker, this will be fixed in a future version of Joomla](https://github.com/joomla-framework/database/pull/38).
As of 3.6.2, out of [Joomla's database drivers](https://github.com/joomla/joomla-cms/tree/3.6.2/libraries/joomla/database/driver), only the PDO driver attempts to support prepared statements. Unfortunately, it is not successful due to a poorly thought out default setting in PHP itself: In order to use *actual* prepared statements, you have to disable *emulated* prepared statements.
Instead of this:
$pdo = new PDO(/* ... */);
Do this:
$pdo = (new PDO(/* ... */))
// Turn off emulated prepares.
->setAttribute(PDO::ATTR_EMULATE_PREPARES, false)
// Optional, but also recommended:
->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
;
As long as the developers working with Joomla's PDO database driver take care to disable emulation, Joomla offers prepared statements. However, this security property isn't included out-of-the-box, and as of 3.6.2, doesn't apply to the default MySQLi driver.
Joomla Doesn't Employ Context-Aware Output Escaping
Modern web applications use templating engines, such as Twig, which provide context-aware output escaping features to mitigate cross-site scripting vulnerabilities.
For example, in the following code snippet, `user_data.title` will be escaped differently than `user_data.body`, particularly with respect to quote characters.
<span title="{{ user_data.title|e('html_attr') }}">{{ user_data.body|e('html') }}</span>
Instead, Joomla just [blacklists HTML tags](https://github.com/joomla/joomla-cms/blob/881021635765e6b055188e6ad7d25d50a6db2cf8/administrator/components/com_templates/helpers/template.php#L151-L174) in an attempt to prevent the low-hanging fruit. Security experts refer to this as **enumerating badness**, which is listed as one of the [six dumbest ideas in computer security](http://www.ranum.com/security/computer_security/editorials/dumb/).
Security Deficits in Drupal's Core
Drupal Does Not Offer Automatic Updates
Drupal doesn't offer automatic security updates. In the event that a security vulnerability is discovered in Drupal and a fix is released, it's up to every individual Drupal site operator to validate and install the update manually. Until the patch is applied, your systems are vulnerable.
[This has already happened once before](http://www.tripwire.com/state-of-security/incident-detection/drupal-psa-if-you-didnt-patch-within-7-hours-consider-your-site-hacked/).
Possibly due to the existence of a historical precedent, some of the Drupal core members are seriously working towards [implementing secure automatic updates into Drupal](https://www.drupal.org/node/2367319). We've offered the team members some guidance on how to proceed (i.e. since libsodium isn't currently an option for most of their userbase, they're stuck with RSA signatures, which even developers with cryptography experience frequently implement incorrectly).
Drupal Almost Offers Prepared Statements
...except [Drupal goes out of its way to use *emulated* prepared statements](https://github.com/drupal/drupal/search?utf8=%E2%9C%93&q=PDO%3A%3AATTR_EMULATE_PREPARES) instead of actual prepared statements, even though [emulated prepared statements suffers from the same fundamental security problem as escaping then concatenating strings](http://stackoverflow.com/a/12202218/2224584). Code and data separation is not upheld.
Drupal uses SHA512Crypt which is Sub-Optimal
There's minor disagreement among cryptographers about which [password hashing functions](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016) will remain strong against hash cracking in the coming years. Of all the acceptable options, PBKDF2 is certainly the weakest one, and SHA512Crypt is [very similar to PBKDF2-SHA512 for practical purposes](http://security.stackexchange.com/a/133251/43688).
Drupal supports a minimum of PHP 5.5, which means they could just as easily migrate to `password_hash()` and `password_verify()`, since those functions are guaranteed to exist. If PHP adopts Argon2i in a future version, Drupal will automatically support it as soon as it becomes the default, with no further code changes necessary.
Everything is Going to Be Okay
All of these security flaws baked into the cornerstones of the software that powers one third of websites on the Internet can be very discouraging. Fortunately, most of these problems are fixable. Refer to how we solved each problem in [CMS Airship](https://github.com/paragonie/airship), for example.
Unfortunately, there are a lot of nontechnical obstacles in the way of making WordPress, Drupal, and Joomla more secure. WordPress developers proudly boast that WordPress powers 1 in 4 websites, and pride themselves on supporting [unsupported versions of PHP](https://secure.php.net/eol.php) as a "usability" feature rather than [a security liability](http://blog.ircmaxell.com/2014/12/on-php-version-requirements.html) that could potentially break the Internet for everyone.
At the end of the day, there are two ways to solve this dilemma:
1. Get the core teams for each large CMS project to take security seriously.
2. Migrate towards a CMS project that already takes security seriously.
We leave answering which solution is better as an exercise for the reader. We're actively pursuing both goals in the hopes that one will [move the needle towards a more secure Internet](https://paragonie.com/blog/2015/12/year-2015-in-review).